diff --git a/extensions/default/src/commandsModule.js b/extensions/default/src/commandsModule.js
index f1eb40d92..9556cc896 100644
--- a/extensions/default/src/commandsModule.js
+++ b/extensions/default/src/commandsModule.js
@@ -1,5 +1,11 @@
const commandsModule = ({ servicesManager, commandsManager }) => {
- const { MeasurementService, ViewportGridService, ToolBarService, CineService } = servicesManager.services;
+ const {
+ MeasurementService,
+ ViewportGridService,
+ ToolBarService,
+ HangingProtocolService,
+ CineService,
+ } = servicesManager.services;
const actions = {
clearMeasurements: () => {
@@ -10,7 +16,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
const { isCineEnabled } = CineService.getState();
CineService.setIsCineEnabled(!isCineEnabled);
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
- viewports.forEach((_, index) => CineService.setCine({ id: index, isPlaying: false }));
+ viewports.forEach((_, index) =>
+ CineService.setCine({ id: index, isPlaying: false })
+ );
+ },
+ nextStage: () => {
+ // next stage in hanging protocols
+ HangingProtocolService.nextProtocolStage();
+ },
+ previousStage: () => {
+ HangingProtocolService.previousProtocolStage();
},
};
@@ -25,6 +40,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
+ nextStage: {
+ commandFn: actions.nextStage,
+ storeContexts: [],
+ options: {},
+ },
+ previousStage: {
+ commandFn: actions.previousStage,
+ storeContexts: [],
+ options: {},
+ },
};
return {
diff --git a/extensions/default/src/getDataSourcesModule.js b/extensions/default/src/getDataSourcesModule.js
index b74d2baf0..d1d4601a5 100644
--- a/extensions/default/src/getDataSourcesModule.js
+++ b/extensions/default/src/getDataSourcesModule.js
@@ -3,6 +3,7 @@
// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance)
import { createDicomWebApi } from './DicomWebDataSource/index.js';
+import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
/**
*
@@ -14,6 +15,11 @@ function getDataSourcesModule() {
type: 'webApi',
createDataSource: createDicomWebApi,
},
+ {
+ name: 'dicomjson',
+ type: 'jsonApi',
+ createDataSource: createDicomJSONApi,
+ },
];
}
diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js
new file mode 100644
index 000000000..0d4e9b297
--- /dev/null
+++ b/extensions/default/src/getHangingProtocolModule.js
@@ -0,0 +1,188 @@
+const hangingProtocolName = 'petCT';
+
+const petCTProtocol = {
+ id: 'PET/CT',
+ locked: true,
+ hasUpdatedPriorsInformation: false,
+ name: 'PET/CT',
+ createdDate: '2021-02-23T18:32:42.849Z',
+ modifiedDate: '2021-02-23T18:32:42.849Z',
+ availableTo: {},
+ editableBy: {},
+ protocolMatchingRules: [
+ {
+ id: 'wauZK2QNEfDPwcAQo',
+ weight: 1,
+ attribute: 'StudyInstanceUID',
+ constraint: {
+ equals: {
+ value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
+ },
+ },
+ required: true,
+ },
+ ],
+ stages: [
+ {
+ id: 'hYbmMy3b7pz7GLiaT',
+ name: 'oneByTwo',
+ viewportStructure: {
+ type: 'grid',
+ properties: {
+ rows: 2,
+ columns: 2,
+ },
+ },
+ viewports: [
+ {
+ viewportSettings: [
+ {
+ options: {
+ voi: {
+ windowWidth: 500,
+ windowCenter: 500,
+ },
+ },
+ commandName: '',
+ // Type can be viewport or prop
+ // viewport: It is most suited for settings that
+ // should be applied before the first render
+ // prop: It is the type of command that can be applied
+ // after the image render, such as tool activations.
+ type: 'viewport',
+ },
+ ],
+ imageMatchingRules: [],
+ seriesMatchingRules: [
+ {
+ id: 'vSjk7NCYjtdS3XZAw',
+ weight: 1,
+ attribute: 'SeriesNumber',
+ constraint: {
+ equals: {
+ value: "4",
+ },
+ },
+ required: false,
+ },
+ ],
+ studyMatchingRules: [],
+ },
+ {
+ viewportSettings: [
+ {
+ options: {
+ invert: true,
+ },
+ commandName: '',
+ type: 'viewport',
+ },
+ ],
+ imageMatchingRules: [
+
+ ],
+ seriesMatchingRules: [
+ {
+ id: 'GPEYqFLv2dwzCM322',
+ weight: 1,
+ attribute: 'SeriesNumber',
+ constraint: {
+ equals: {
+ value: "1",
+ },
+ },
+ required: false,
+ },
+ ],
+ studyMatchingRules: [
+
+ ],
+ },
+ {
+ viewportSettings: [
+ {
+ options: {
+ invert: true,
+ },
+ commandName: '',
+ type: 'viewport',
+ },
+ ],
+ imageMatchingRules: [
+
+ ],
+ seriesMatchingRules: [
+ {
+ id: 'GPEYqFLv2dwzCM322',
+ weight: 1,
+ attribute: 'SeriesNumber',
+ constraint: {
+ equals: {
+ value: "6",
+ },
+ },
+ required: false,
+ },
+ ],
+ studyMatchingRules: [
+
+ ],
+ },
+ {
+ viewportSettings: [
+ {
+ options: {
+ invert: true,
+ },
+ commandName: '',
+ type: 'viewport',
+ },
+ ],
+ imageMatchingRules: [
+
+ ],
+ seriesMatchingRules: [
+ {
+ id: 'GPEYqFLv2dwzCM322',
+ weight: 1,
+ attribute: 'SeriesDescription',
+ constraint: {
+ contains: {
+ value: "Corrected",
+ },
+ },
+ required: false,
+ },
+ {
+ id: 'GPEYqFLv2dwzCM322',
+ weight: 2,
+ attribute: 'SeriesDescription',
+ constraint: {
+ doesNotContain: {
+ value: "Uncorrected",
+ },
+ },
+ required: false,
+ },
+ ],
+ studyMatchingRules: [
+
+ ],
+ },
+ ],
+ createdDate: '2021-02-23T18:32:42.850Z',
+ },
+ ],
+ numberOfPriorsReferenced: 0,
+};
+
+function getHangingProtocolModule() {
+ return [
+ {
+ name: hangingProtocolName,
+ protocols: [petCTProtocol],
+ },
+ ];
+}
+
+export default getHangingProtocolModule;
diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js
index dcaf70eef..cc4b39ae3 100644
--- a/extensions/default/src/index.js
+++ b/extensions/default/src/index.js
@@ -3,6 +3,7 @@ import getDataSourcesModule from './getDataSourcesModule.js';
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
import getPanelModule from './getPanelModule.js';
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
+import getHangingProtocolModule from './getHangingProtocolModule.js';
import getToolbarModule from './getToolbarModule.js';
import commandsModule from './commandsModule';
import id from './id';
@@ -14,6 +15,7 @@ export default {
id,
getContextModule,
getDataSourcesModule,
+ getHangingProtocolModule,
getLayoutTemplateModule,
getPanelModule,
getSopClassHandlerModule,
diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json
index ba35bbcb7..626dac009 100644
--- a/extensions/dicom-sr/package.json
+++ b/extensions/dicom-sr/package.json
@@ -32,7 +32,7 @@
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
- "cornerstone-math": "^0.1.8",
+ "cornerstone-math": "^0.1.9",
"cornerstone-tools": "5.1.2",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.16.1",
@@ -40,7 +40,7 @@
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^16.11.0",
- "react-cornerstone-viewport": "4.0.2"
+ "react-cornerstone-viewport": "4.0.5"
},
"dependencies": {
"@babel/runtime": "7.7.6",
diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json
index 45f62bd2b..e94a365fc 100644
--- a/extensions/measurement-tracking/package.json
+++ b/extensions/measurement-tracking/package.json
@@ -34,7 +34,7 @@
"dcmjs": "0.16.1",
"prop-types": "^15.6.2",
"react": "^16.13.1",
- "react-cornerstone-viewport": "^4.0.4",
+ "react-cornerstone-viewport": "^4.0.5",
"react-dom": "^16.13.1",
"webpack": "^4.0.0",
"webpack-merge": "^5.7.3"
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
index f8293766f..1cce041e0 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
@@ -12,7 +12,6 @@ import promptTrackNewSeries from './promptTrackNewSeries';
import promptTrackNewStudy from './promptTrackNewStudy';
import promptSaveReport from './promptSaveReport';
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
-import hydrateStructuredReport from './_hydrateStructuredReport.js';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
@@ -99,6 +98,7 @@ function TrackedMeasurementsContextProvider(
}),
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
servicesManager,
+ extensionManager
}),
});
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js
index 9d308b0c0..2baceb80a 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js
@@ -1,7 +1,7 @@
import cornerstoneTools from 'cornerstone-tools';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
-import getToolStateToCornerstoneMeasurementSchema from './getToolStateToCornerstoneMeasurementSchema';
+import getCornerstoneToolStateToMeasurementSchema from './getCornerstoneToolStateToMeasurementSchema';
import { adapters } from 'dcmjs';
const { guid } = OHIF.utils;
@@ -15,6 +15,7 @@ export default function _hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
) {
+ const dataSource = extensionManager.getActiveDataSource()[0];
const { MeasurementService, DisplaySetService } = servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID(
@@ -114,14 +115,23 @@ export default function _hydrateStructuredReport(
data.id = guid();
- _addToolDataToCornerstoneTools(data, toolType, imageId);
+ const instance = cornerstone.metaData.get('instance', imageId);
+ const {
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
+ } = instance;
// Let the measurement service know we added to toolState
- const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
+ const toMeasurementSchema = getCornerstoneToolStateToMeasurementSchema(
toolType,
MeasurementService,
DisplaySetService,
- imageId
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID
);
const source = MeasurementService.getSource('CornerstoneTools', '4');
@@ -132,7 +142,8 @@ export default function _hydrateStructuredReport(
source,
toolType,
data,
- toMeasurementSchema
+ toMeasurementSchema,
+ dataSource
);
if (!imageIds.includes(imageId)) {
@@ -148,24 +159,3 @@ export default function _hydrateStructuredReport(
SeriesInstanceUIDs,
};
}
-
-function _addToolDataToCornerstoneTools(data, toolType, imageId) {
- const toolState = globalImageIdSpecificToolStateManager.saveToolState();
-
- if (toolState[imageId] === undefined) {
- toolState[imageId] = {};
- }
-
- const imageIdToolState = toolState[imageId];
-
- // If we don't have tool state for this type of tool, add an empty object
- if (imageIdToolState[toolType] === undefined) {
- imageIdToolState[toolType] = {
- data: [],
- };
- }
-
- const toolData = imageIdToolState[toolType];
-
- toolData.data.push(data);
-}
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getToolStateToCornerstoneMeasurementSchema.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js
similarity index 87%
rename from extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getToolStateToCornerstoneMeasurementSchema.js
rename to extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js
index da8f12030..c548a7929 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getToolStateToCornerstoneMeasurementSchema.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js
@@ -1,8 +1,11 @@
-export default function getToolStateToCornerstoneMeasurementSchema(
+export default function getCornerstoneToolStateToMeasurementSchema(
toolType,
MeasurementService,
DisplaySetService,
- imageId
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID
) {
const _getValueTypeFromToolType = toolType => {
const {
@@ -31,7 +34,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
return measurementData =>
Length(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
@@ -39,7 +45,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
return measurementData =>
Bidirectional(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
@@ -47,7 +56,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
return measurementData =>
EllipticalRoi(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
@@ -55,7 +67,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
return measurementData =>
ArrowAnnotate(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
@@ -64,18 +79,14 @@ export default function getToolStateToCornerstoneMeasurementSchema(
function Length(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
- const instance = cornerstone.metaData.get('instance', imageId);
- const {
- SOPInstanceUID,
- FrameOfReferenceUID,
- SeriesInstanceUID,
- StudyInstanceUID,
- } = instance;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
@@ -113,18 +124,15 @@ function Length(
function Bidirectional(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
- const instance = cornerstone.metaData.get('instance', imageId);
- const {
- SOPInstanceUID,
- FrameOfReferenceUID,
- SeriesInstanceUID,
- StudyInstanceUID,
- } = instance;
+
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
@@ -155,18 +163,14 @@ function Bidirectional(
function EllipticalRoi(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
- const instance = cornerstone.metaData.get('instance', imageId);
- const {
- SOPInstanceUID,
- FrameOfReferenceUID,
- SeriesInstanceUID,
- StudyInstanceUID,
- } = instance;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
@@ -222,18 +226,14 @@ function EllipticalRoi(
function ArrowAnnotate(
measurementData,
- imageId,
+ SOPInstanceUID,
+ FrameOfReferenceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
- const instance = cornerstone.metaData.get('instance', imageId);
- const {
- SOPInstanceUID,
- FrameOfReferenceUID,
- SeriesInstanceUID,
- StudyInstanceUID,
- } = instance;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
index ce4b17ef2..c44cae8ff 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
@@ -6,7 +6,7 @@ const RESPONSE = {
SET_STUDY_AND_SERIES: 3,
};
-function promptUser({ servicesManager }, ctx, evt) {
+function promptUser({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
diff --git a/extensions/measurement-tracking/src/getViewportModule.js b/extensions/measurement-tracking/src/getViewportModule.js
index e01118421..c9974fb9a 100644
--- a/extensions/measurement-tracking/src/getViewportModule.js
+++ b/extensions/measurement-tracking/src/getViewportModule.js
@@ -12,12 +12,13 @@ const OHIFCornerstoneViewport = props => {
);
};
-function getViewportModule({ servicesManager, commandsManager }) {
+function getViewportModule({ servicesManager, commandsManager, extensionManager }) {
const ExtendedOHIFCornerstoneTrackingViewport = props => {
return (
);
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
index 333f36b6a..924afbcb9 100644
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
@@ -114,15 +114,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
// Are we exposing the right API for measurementService?
// This watches for ALL MeasurementService changes. It updates a timestamp,
// which is debounced. After a brief period of inactivity, this triggers
- // a re-render where we grab up-to-date measurements.
+ // a re-render where we grab up-to-date measurements
useEffect(() => {
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
+ const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
const subscriptions = [];
- [added, updated, removed, cleared].forEach(evt => {
+ [added, addedRaw, updated, removed, cleared].forEach(evt => {
subscriptions.push(
MeasurementService.subscribe(evt, () => {
setMeasurementsUpdated(Date.now().toString());
diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
index fc4a21944..8b064d303 100644
--- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
+++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
@@ -28,7 +28,7 @@ function PanelStudyBrowserTracking({
// Normally you nest the components so the tree isn't so deep, and the data
// doesn't have to have such an intense shape. This works well enough for now.
// Tabs --> Studies --> DisplaySets --> Thumbnails
- const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer();
+ const { StudyInstanceUIDs } = useImageViewer();
const [
{ activeViewportIndex, viewports, numCols, numRows },
viewportGridService,
@@ -58,27 +58,34 @@ function PanelStudyBrowserTracking({
const isSingleViewport = numCols === 1 && numRows === 1;
- // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy?
useEffect(() => {
- const { unsubscribe } = MeasurementService.subscribe(
- MeasurementService.EVENTS.MEASUREMENT_ADDED,
- ({ source, measurement }) => {
- const {
- referenceSeriesUID: SeriesInstanceUID,
- referenceStudyUID: StudyInstanceUID,
- } = measurement;
+ const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
+ const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
+ const subscriptions = [];
- sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
- sendTrackedMeasurementsEvent('TRACK_SERIES', {
- viewportIndex: activeViewportIndex,
- StudyInstanceUID,
- SeriesInstanceUID,
- });
- }
- );
+ [added, addedRaw].forEach(evt => {
+ subscriptions.push(
+ MeasurementService.subscribe(evt, ({ source, measurement }) => {
+ const {
+ referenceSeriesUID: SeriesInstanceUID,
+ referenceStudyUID: StudyInstanceUID,
+ } = measurement;
- return unsubscribe;
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
+ sendTrackedMeasurementsEvent('TRACK_SERIES', {
+ viewportIndex: activeViewportIndex,
+ StudyInstanceUID,
+ SeriesInstanceUID,
+ });
+ }).unsubscribe
+ );
+ });
+
+ return () => {
+ subscriptions.forEach(unsub => {
+ unsub();
+ });
+ };
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
@@ -89,7 +96,6 @@ function PanelStudyBrowserTracking({
async function fetchStudiesForPatient(StudyInstanceUID) {
const qidoStudiesForPatient =
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
-
// TODO: This should be "naturalized DICOM JSON" studies
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
@@ -182,6 +188,7 @@ function PanelStudyBrowserTracking({
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
+
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
if (imageId) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js
index 9327383ee..0c2fdfdd2 100644
--- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js
+++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js
@@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
+ if (StudyInstanceUID === undefined) return;
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?
diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js
index 2fd411e88..1476a3298 100644
--- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js
+++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js
@@ -1,8 +1,7 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
-import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { utils } from '@ohif/core';
import {
Notification,
@@ -12,14 +11,10 @@ import {
useViewportDialog,
} from '@ohif/ui';
import { useTrackedMeasurements } from './../getContextModule';
-
-import ViewportOverlay from './ViewportOverlay';
-import ViewportLoadingIndicator from './ViewportLoadingIndicator';
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
import getTools from '../_shared/getTools';
-const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
const { formatDate } = utils;
// TODO -> Get this list from the list of tracked measurements.
@@ -40,19 +35,19 @@ const BaseAnnotationTool = cornerstoneTools.importInternal(
const { StackManager } = OHIF.utils;
-function TrackedCornerstoneViewport({
- children,
- dataSource,
- displaySet,
- viewportIndex,
- servicesManager,
- commandsManager,
-}) {
+function TrackedCornerstoneViewport(props) {
const {
- ToolBarService,
- DisplaySetService,
- MeasurementService,
- } = servicesManager.services;
+ children,
+ dataSource,
+ displaySet,
+ viewportIndex,
+ servicesManager,
+ extensionManager,
+ commandsManager,
+ } = props;
+
+ const { ToolBarService } = servicesManager.services;
+
const [trackedMeasurements] = useTrackedMeasurements();
const [
{ activeViewportIndex, viewports },
@@ -60,73 +55,9 @@ function TrackedCornerstoneViewport({
] = useViewportGrid();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
- const [viewportData, setViewportData] = useState(null);
- const [element, setElement] = useState(null);
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
-
- // TODO: Still needed? Better way than import `OHIF` and destructure?
- // Why is this managed by `core`?
- useEffect(() => {
- return () => {
- StackManager.clearStacks();
- };
- }, []);
-
- useEffect(() => {
- cineService.setCine({ id: viewportIndex });
- }, [viewportIndex]);
-
- useEffect(() => {
- const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
- MeasurementService,
- DisplaySetService,
- element,
- viewportIndex,
- displaySet.displaySetInstanceUID
- );
-
- _checkForCachedJumpToMeasurementEvents(
- MeasurementService,
- DisplaySetService,
- element,
- viewportIndex,
- displaySet.displaySetInstanceUID
- );
-
- return () => {
- unsubscribeFromJumpToMeasurementEvents();
- };
- }, [element, displaySet]);
-
- useEffect(() => {
- if (!element) {
- return;
- }
- const allTools = cornerstoneTools.store.state.tools;
- const toolsForElement = allTools.filter(tool => tool.element === element);
-
- toolsForElement.forEach(tool => {
- if (
- tool instanceof ArrowAnnotateTool ||
- tool instanceof BidirectionalTool ||
- tool instanceof EllipticalRoiTool ||
- tool instanceof LengthTool
- ) {
- const configuration = tool.configuration;
-
- configuration.renderDashed = !isTracked;
-
- tool.configuration = configuration;
- }
- });
-
- const enabledElement = cornerstone.getEnabledElement(element);
-
- if (enabledElement.image) {
- cornerstone.updateImage(element);
- }
- }, [isTracked]);
+ const [element, setElement] = useState(null);
const onElementEnabled = evt => {
const eventData = evt.detail;
@@ -191,51 +122,33 @@ function TrackedCornerstoneViewport({
};
useEffect(() => {
- const {
- StudyInstanceUID,
- displaySetInstanceUID,
- sopClassUids,
- } = displaySet;
-
- if (!StudyInstanceUID || !displaySetInstanceUID) {
+ if (!element) {
return;
}
+ const allTools = cornerstoneTools.store.state.tools;
+ const toolsForElement = allTools.filter(tool => tool.element === element);
- if (sopClassUids && sopClassUids.length > 1) {
- console.warn(
- 'More than one SOPClassUID in the same series is not yet supported.'
- );
- }
+ toolsForElement.forEach(tool => {
+ if (
+ tool instanceof ArrowAnnotateTool ||
+ tool instanceof BidirectionalTool ||
+ tool instanceof EllipticalRoiTool ||
+ tool instanceof LengthTool
+ ) {
+ const configuration = tool.configuration;
- _getViewportData(dataSource, displaySet).then(setViewportData);
- }, [dataSource, displaySet, viewports, viewportIndex]);
+ configuration.renderDashed = !isTracked;
- // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- let childrenWithProps = null;
-
- if (!viewportData) {
- return null;
- }
-
- const {
- imageIds,
- currentImageIdIndex,
- // If this comes from the instance, would be a better default
- // `FrameTime` in the instance
- // frameRate = 0,
- } = viewportData.stack;
-
- if (children && children.length) {
- childrenWithProps = children.map((child, index) => {
- return (
- child &&
- React.cloneElement(child, {
- viewportIndex,
- key: index,
- })
- );
+ tool.configuration = configuration;
+ }
});
- }
+
+ const enabledElement = cornerstone.getEnabledElement(element);
+
+ if (enabledElement.image) {
+ cornerstone.updateImage(element);
+ }
+ }, [isTracked]);
// We have...
// StudyInstanceUid, DisplaySetInstanceUid
@@ -313,6 +226,19 @@ function TrackedCornerstoneViewport({
);
}
+ const renderViewport = () => {
+ const { component: Component } = extensionManager.getModuleEntry(
+ 'org.ohif.cornerstone.viewportModule.cornerstone'
+ );
+ return (
+
+ );
+ };
+
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
@@ -341,10 +267,12 @@ function TrackedCornerstoneViewport({
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
- thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
+ thickness: SliceThickness
+ ? `${parseFloat(SliceThickness).toFixed(2)}mm`
+ : '',
spacing:
SpacingBetweenSlices !== undefined
- ? `${SpacingBetweenSlices.toFixed(2)}mm`
+ ? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
@@ -362,29 +290,7 @@ function TrackedCornerstoneViewport({
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
-
{
- return (
-
- );
- }}
- />
+ {renderViewport()}
{viewportDialogState.viewportIndex === viewportIndex && (
)}
- {childrenWithProps}
>
);
@@ -417,54 +322,6 @@ TrackedCornerstoneViewport.defaultProps = {
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
-/**
- * Obtain the CornerstoneTools Stack for the specified display set.
- *
- * @param {Object} displaySet
- * @param {Object} dataSource
- * @return {Object} CornerstoneTools Stack
- */
-function _getCornerstoneStack(displaySet, dataSource) {
- // Get stack from Stack Manager
- const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
-
- // Clone the stack here so we don't mutate it
- const stack = Object.assign({}, storedStack);
-
- return stack;
-}
-
-// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
-function _onDoubleClick() {
- const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
- 'manipulatorState'
- ).setters.cancelActiveManipulatorsForElement;
- const enabledElements = cornerstoneTools.store.state.enabledElements;
- enabledElements.forEach(element => {
- cancelActiveManipulatorsForElement(element);
- });
-}
-
-/**
- * Builds the viewport data from a datasource and a displayset.
- *
- * @param {Object} dataSource
- * @param {Object} displaySet
- * @return {Object} viewport data
- */
-
-async function _getViewportData(dataSource, displaySet) {
- const stack = _getCornerstoneStack(displaySet, dataSource);
-
- const viewportData = {
- StudyInstanceUID: displaySet.StudyInstanceUID,
- displaySetInstanceUID: displaySet.displaySetInstanceUID,
- stack,
- };
-
- return viewportData;
-}
-
function _getNextMeasurementId(
direction,
servicesManager,
@@ -518,112 +375,4 @@ function _getNextMeasurementId(
return newTrackedMeasurementId;
}
-function _subscribeToJumpToMeasurementEvents(
- MeasurementService,
- DisplaySetService,
- element,
- viewportIndex,
- displaySetInstanceUID
-) {
- const { unsubscribe } = MeasurementService.subscribe(
- MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
- ({ viewportIndex: jumpToMeasurementViewportIndex, measurement }) => {
- // check if the correct viewport index.
- if (viewportIndex !== jumpToMeasurementViewportIndex) {
- // Event for a different viewport.
- return;
- }
-
- if (measurement.displaySetInstanceUID !== displaySetInstanceUID) {
- // Not for this displaySet.
- return;
- }
-
- _jumpToMeasurement(
- measurement,
- element,
- viewportIndex,
- MeasurementService,
- DisplaySetService
- );
- }
- );
-
- return unsubscribe;
-}
-
-function _checkForCachedJumpToMeasurementEvents(
- MeasurementService,
- DisplaySetService,
- element,
- viewportIndex,
- displaySetInstanceUID
-) {
- // Check if there is a queued jumpToMeasurement event
- const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
- viewportIndex
- );
-
- if (measurementIdToJumpTo && element) {
- // Jump to measurement if the measurement exists
- const measurement = MeasurementService.getMeasurement(
- measurementIdToJumpTo
- );
-
- if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
- _jumpToMeasurement(
- measurement,
- element,
- viewportIndex,
- MeasurementService,
- DisplaySetService
- );
- }
- }
-}
-
-function _jumpToMeasurement(
- measurement,
- targetElement,
- viewportIndex,
- MeasurementService,
- DisplaySetService
-) {
- const { displaySetInstanceUID, SOPInstanceUID } = measurement;
-
- const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
- displaySetInstanceUID
- );
-
- const imageIndex = referencedDisplaySet.images.findIndex(
- i => i.SOPInstanceUID === SOPInstanceUID
- );
-
- setCornerstoneMeasurementActive(measurement);
-
- if (targetElement !== null) {
- const enabledElement = cornerstone.getEnabledElement(targetElement);
-
- // Wait for the image to update or we get a race condition when the element has only just been enabled.
- const scrollToHandler = evt => {
- scrollToIndex(targetElement, imageIndex);
- targetElement.removeEventListener(
- cornerstone.EVENTS.IMAGE_RENDERED,
- scrollToHandler
- );
- };
- targetElement.addEventListener(
- cornerstone.EVENTS.IMAGE_RENDERED,
- scrollToHandler
- );
-
- if (enabledElement.image) {
- cornerstone.updateImage(targetElement);
- }
-
- // Jump to measurement consumed, remove.
- MeasurementService.removeJumpToMeasurement(viewportIndex);
- }
-}
-
export default TrackedCornerstoneViewport;
diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js
index 6306fa19c..0bd9e4b81 100644
--- a/modes/longitudinal/src/index.js
+++ b/modes/longitudinal/src/index.js
@@ -4,6 +4,7 @@ import { hotkeys } from '@ohif/core';
const ohif = {
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
+ hangingProtocols: 'org.ohif.default.hangingProtocolModule.default',
};
const tracked = {
@@ -17,6 +18,7 @@ const dicomsr = {
viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr',
};
+
export default function mode({ modeConfiguration }) {
return {
// TODO: We're using this as a route segment
@@ -40,6 +42,18 @@ export default function mode({ modeConfiguration }) {
};
ToolBarService.recordInteraction(interaction);
+
+ ToolBarService.init(extensionManager);
+ ToolBarService.addButtons(toolbarButtons);
+ ToolBarService.createButtonSection('primary', [
+ 'MeasurementTools',
+ 'Zoom',
+ 'WindowLevel',
+ 'Pan',
+ 'Capture',
+ 'Layout',
+ 'MoreTools',
+ ]);
},
onModeExit: () => {},
validationTags: {
@@ -53,21 +67,10 @@ export default function mode({ modeConfiguration }) {
routes: [
{
path: 'longitudinal',
- init: ({ servicesManager, extensionManager }) => {
- const { ToolBarService } = servicesManager.services;
- ToolBarService.init(extensionManager);
- ToolBarService.addButtons(toolbarButtons);
- ToolBarService.createButtonSection('primary', [
- 'MeasurementTools',
- 'Zoom',
- 'WindowLevel',
- 'Pan',
- 'Capture',
- 'Layout',
- 'MoreTools',
- ]);
- },
- layoutTemplate: ({ routeProps }) => {
+ /*init: ({ servicesManager, extensionManager }) => {
+ //defaultViewerRouteInit
+ },*/
+ layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
@@ -95,6 +98,7 @@ export default function mode({ modeConfiguration }) {
'org.ohif.measurement-tracking',
'org.ohif.dicom-sr',
],
+ hangingProtocols: [ohif.hangingProtocols],
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
diff --git a/package.json b/package.json
index bbf0a85a5..90b8c93fc 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,7 @@
"cross-env": "^5.2.0",
"css-loader": "^3.2.0",
"dotenv": "^8.1.0",
- "eslint": "5.16.0",
+ "eslint": "6.8.0",
"eslint-config-prettier": "^6.4.0",
"eslint-config-react-app": "^5.2.0",
"eslint-plugin-flowtype": "2.x",
@@ -84,7 +84,7 @@
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "7.x",
- "eslint-plugin-react-hooks": "1.5.0",
+ "eslint-plugin-react-hooks": "4.2.0",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"html-webpack-plugin": "^3.2.0",
"husky": "^3.0.0",
diff --git a/platform/core/package.json b/platform/core/package.json
index a5a2cc6ca..a2071176e 100644
--- a/platform/core/package.json
+++ b/platform/core/package.json
@@ -45,7 +45,8 @@
"mousetrap": "^1.6.3",
"query-string": "^6.14.0",
"object-hash": "2.1.1",
- "cornerstone-math": "0.1.9"
+ "cornerstone-math": "0.1.9",
+ "validate.js": "^0.12.0"
},
"devDependencies": {
"webpack-merge": "5.7.3"
diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js
index 82c9facab..fda0fca22 100644
--- a/platform/core/src/DataSources/IWebApiDataSource.js
+++ b/platform/core/src/DataSources/IWebApiDataSource.js
@@ -17,9 +17,11 @@ function create({
retrieve,
store,
reject,
+ parseRouteParams,
retrieveSeriesMetadata,
deleteStudyMetadataPromise,
getImageIdsForDisplaySet,
+ getImageIdsForInstance,
}) {
const defaultQuery = {
studies: {
@@ -56,15 +58,16 @@ function create({
};
const defaultReject = {};
-
return {
query: query || defaultQuery,
retrieve: retrieve || defaultRetrieve,
reject: reject || defaultReject,
store: store || defaultStore,
- getImageIdsForDisplaySet,
+ parseRouteParams,
retrieveSeriesMetadata,
deleteStudyMetadataPromise,
+ getImageIdsForDisplaySet,
+ getImageIdsForInstance,
};
}
diff --git a/platform/core/src/classes/MetadataProvider.js b/platform/core/src/classes/MetadataProvider.js
index 7c4cc555e..e6f2c739f 100644
--- a/platform/core/src/classes/MetadataProvider.js
+++ b/platform/core/src/classes/MetadataProvider.js
@@ -189,7 +189,7 @@ class MetadataProvider {
metadata = {
modality: instance.Modality,
seriesInstanceUID: instance.SeriesInstanceUID,
- seriesNumber: instance.SeriesNumber,
+ seriesNumber: getNumberValues(instance.SeriesNumber),
studyInstanceUID: instance.StudyInstanceUID,
seriesDate,
seriesTime,
@@ -197,9 +197,9 @@ class MetadataProvider {
break;
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
metadata = {
- patientAge: instance.PatientAge,
- patientSize: instance.PatientSize,
- patientWeight: instance.PatientWeight,
+ patientAge: getNumberValues(instance.PatientAge),
+ patientSize: getNumberValues(instance.PatientSize),
+ patientWeight: getNumberValues(instance.PatientWeight),
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
@@ -228,51 +228,53 @@ class MetadataProvider {
metadata = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
- rows: instance.Rows,
- columns: instance.Columns,
- imageOrientationPatient: ImageOrientationPatient,
- rowCosines,
- columnCosines,
- imagePositionPatient: instance.ImagePositionPatient,
- sliceThickness: instance.SliceThickness,
- sliceLocation: instance.SliceLocation,
- pixelSpacing: PixelSpacing,
- rowPixelSpacing,
- columnPixelSpacing,
+ rows: getNumberValues(instance.Rows),
+ columns: getNumberValues(instance.Columns),
+ imageOrientationPatient: getNumberValues(ImageOrientationPatient),
+ rowCosines: getNumberValues(rowCosines),
+ columnCosines: getNumberValues(columnCosines),
+ imagePositionPatient: getNumberValues(instance.ImagePositionPatient),
+ sliceThickness: getNumberValues(instance.SliceThickness),
+ sliceLocation: getNumberValues(instance.SliceLocation),
+ pixelSpacing: getNumberValues(PixelSpacing),
+ rowPixelSpacing: getNumberValues(rowPixelSpacing),
+ columnPixelSpacing: getNumberValues(columnPixelSpacing),
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
metadata = {
- samplesPerPixel: instance.SamplesPerPixel,
- photometricInterpretation: instance.PhotometricInterpretation,
- rows: instance.Rows,
- columns: instance.Columns,
- bitsAllocated: instance.BitsAllocated,
- bitsStored: instance.BitsStored,
- highBit: instance.HighBit,
- pixelRepresentation: instance.PixelRepresentation,
- planarConfiguration: instance.PlanarConfiguration,
- pixelAspectRatio: instance.PixelAspectRatio,
- smallestPixelValue: instance.SmallestPixelValue,
- largestPixelValue: instance.LargestPixelValue,
+ samplesPerPixel: getNumberValues(instance.SamplesPerPixel),
+ photometricInterpretation: getNumberValues(instance.PhotometricInterpretation),
+ rows: getNumberValues(instance.Rows),
+ columns: getNumberValues(instance.Columns),
+ bitsAllocated: getNumberValues(instance.BitsAllocated),
+ bitsStored: getNumberValues(instance.BitsStored),
+ highBit: getNumberValues(instance.HighBit),
+ pixelRepresentation: getNumberValues(instance.PixelRepresentation),
+ planarConfiguration: getNumberValues(instance.PlanarConfiguration),
+ pixelAspectRatio: getNumberValues(instance.PixelAspectRatio),
+ smallestPixelValue: getNumberValues(instance.SmallestPixelValue),
+ largestPixelValue: getNumberValues(instance.LargestPixelValue),
redPaletteColorLookupTableDescriptor:
- instance.RedPaletteColorLookupTableDescriptor,
+ getNumberValues(instance.RedPaletteColorLookupTableDescriptor),
greenPaletteColorLookupTableDescriptor:
- instance.GreenPaletteColorLookupTableDescriptor,
+ getNumberValues(instance.GreenPaletteColorLookupTableDescriptor),
bluePaletteColorLookupTableDescriptor:
- instance.BluePaletteColorLookupTableDescriptor,
+ getNumberValues(instance.BluePaletteColorLookupTableDescriptor),
redPaletteColorLookupTableData:
- instance.RedPaletteColorLookupTableData,
+ getNumberValues(instance.RedPaletteColorLookupTableData),
greenPaletteColorLookupTableData:
- instance.GreenPaletteColorLookupTableData,
+ getNumberValues(instance.GreenPaletteColorLookupTableData),
bluePaletteColorLookupTableData:
- instance.BluePaletteColorLookupTableData,
+ getNumberValues(instance.BluePaletteColorLookupTableData),
};
break;
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
const { WindowCenter, WindowWidth } = instance;
-
+ if (WindowCenter === undefined || WindowWidth === undefined) {
+ return;
+ }
const windowCenter = Array.isArray(WindowCenter)
? WindowCenter
: [WindowCenter];
@@ -281,15 +283,20 @@ class MetadataProvider {
: [WindowWidth];
metadata = {
- windowCenter,
- windowWidth,
+ windowCenter: getNumberValues(windowCenter),
+ windowWidth: getNumberValues(windowWidth),
};
break;
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
+ const { RescaleIntercept, RescaleSlope } = instance;
+ if (RescaleIntercept === undefined || RescaleSlope === undefined) {
+ return;
+ }
+
metadata = {
- rescaleIntercept: instance.RescaleIntercept,
- rescaleSlope: instance.RescaleSlope,
+ rescaleIntercept: getNumberValues(instance.RescaleIntercept),
+ rescaleSlope: getNumberValues(instance.RescaleSlope),
rescaleType: instance.RescaleType,
};
break;
@@ -401,7 +408,7 @@ class MetadataProvider {
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
metadata = {
sopInstanceUid: instance.SOPInstanceUID,
- instanceNumber: instance.InstanceNumber,
+ instanceNumber: getNumberValues(instance.InstanceNumber),
lossyImageCompression: instance.LossyImageCompression,
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
@@ -457,6 +464,31 @@ class MetadataProvider {
const metadataProvider = new MetadataProvider();
+
+/**
+ * Returns the values as an array of javascript numbers
+ *
+ * @param element - The javascript object for the specified element in the metadata
+ * @returns {*}
+ */
+function getNumberValues(element) {
+ if (!element) {
+ return;
+ }
+
+ if (Array.isArray(element)) {
+ const values = [];
+ for (let i = 0; i < element.length; i++) {
+ values.push(parseFloat(element[i]));
+ }
+ return values;
+ }
+
+ return parseFloat(element)
+}
+
+
+
export default metadataProvider;
const WADO_IMAGE_LOADER_TAGS = {
@@ -479,3 +511,4 @@ const WADO_IMAGE_LOADER_TAGS = {
};
const INSTANCE = 'instance';
+const DICOMWEB = 'dicomweb';
diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js
index 0cba1a3d6..e9299fd3b 100644
--- a/platform/core/src/extensions/ExtensionManager.js
+++ b/platform/core/src/extensions/ExtensionManager.js
@@ -2,7 +2,12 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js';
export default class ExtensionManager {
- constructor({ commandsManager, servicesManager, hotkeysManager, appConfig = {} }) {
+ constructor({
+ commandsManager,
+ servicesManager,
+ hotkeysManager,
+ appConfig = {},
+ }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
@@ -52,7 +57,7 @@ export default class ExtensionManager {
onModeEnter({
servicesManager: _servicesManager,
commandsManager: _commandsManager,
- hotkeysManager: _hotkeysManager
+ hotkeysManager: _hotkeysManager,
});
}
});
@@ -115,9 +120,7 @@ export default class ExtensionManager {
*/
registerExtension = (extension, configuration = {}, dataSources = []) => {
if (!extension) {
- throw new Error(
- 'Attempting to register a null/undefined extension.'
- );
+ throw new Error('Attempting to register a null/undefined extension.');
}
let extensionId = extension.id;
@@ -191,6 +194,13 @@ export default class ExtensionManager {
] = element;
});
break;
+ case MODULE_TYPES.HANGING_PROTOCOL:
+ extensionModule.forEach(element => {
+ this.modulesMap[
+ `${extensionId}.${moduleType}.${element.name}`
+ ] = element;
+ });
+ break;
default:
throw new Error(`Module type invalid: ${moduleType}`);
}
@@ -224,6 +234,10 @@ export default class ExtensionManager {
return this.dataSourceMap[this.activeDataSource];
};
+ getDataSource = () => {
+ return this.dataSourceMap[this.activeDataSource];
+ };
+
/**
* @private
* @param {string} moduleType
@@ -288,6 +302,32 @@ export default class ExtensionManager {
});
}
+ _initHangingProtocolModule(extensionModule, extensionId) {
+ extensionModule.forEach(element => {
+ const namespace = `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`;
+
+ dataSources.forEach(dataSource => {
+ if (dataSource.namespace === namespace) {
+ const dataSourceInstance = element.createDataSource(
+ dataSource.configuration
+ );
+
+ if (this.dataSourceMap[dataSource.sourceName]) {
+ this.dataSourceMap[dataSource.sourceName].push(dataSourceInstance);
+ } else {
+ this.dataSourceMap[dataSource.sourceName] = [dataSourceInstance];
+ }
+ }
+ });
+ });
+
+ extensionModule.forEach(element => {
+ this.modulesMap[
+ `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`
+ ] = element;
+ });
+ }
+
/**
*
* @private
diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js
index b5184127d..174a334e3 100644
--- a/platform/core/src/extensions/ExtensionManager.test.js
+++ b/platform/core/src/extensions/ExtensionManager.test.js
@@ -215,6 +215,9 @@ describe('ExtensionManager.js', () => {
getDataSourcesModule: () => {
return [{}];
},
+ getHangingProtocolModule: () => {
+ return [{}];
+ },
getContextModule: () => {
return [{}];
}
diff --git a/platform/core/src/extensions/MODULE_TYPES.js b/platform/core/src/extensions/MODULE_TYPES.js
index 887347aba..d77cced16 100644
--- a/platform/core/src/extensions/MODULE_TYPES.js
+++ b/platform/core/src/extensions/MODULE_TYPES.js
@@ -7,4 +7,5 @@ export default {
VIEWPORT: 'viewportModule',
CONTEXT: 'contextModule',
LAYOUT_TEMPLATE: 'layoutTemplateModule',
+ HANGING_PROTOCOL: 'hangingProtocolModule',
};
diff --git a/platform/core/src/index.js b/platform/core/src/index.js
index f695e6a24..ff33f36bc 100644
--- a/platform/core/src/index.js
+++ b/platform/core/src/index.js
@@ -25,6 +25,7 @@ import {
MeasurementService,
ViewportGridService,
HangingProtocolService,
+ pubSubServiceInterface,
} from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
@@ -67,6 +68,7 @@ const OHIF = {
HangingProtocolService,
IWebApiDataSource,
DicomMetadataStore,
+ pubSubServiceInterface,
};
export {
@@ -101,6 +103,7 @@ export {
HangingProtocolService,
IWebApiDataSource,
DicomMetadataStore,
+ pubSubServiceInterface,
};
export { OHIF };
diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js
index a5ccfe6ac..ad97e3ff1 100644
--- a/platform/core/src/index.test.js
+++ b/platform/core/src/index.test.js
@@ -10,21 +10,6 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
- 'DicomMetadataStore',
- //
- 'CineService',
- 'DisplaySetService',
- 'HangingProtocolService',
- 'ToolBarService',
- 'UINotificationService',
- 'UIModalService',
- 'UIDialogService',
- 'UIViewportDialogService',
- 'MeasurementService',
- 'ViewportGridService',
- //
- 'IWebApiDataSource',
- //
'defaults',
'utils',
'hotkeys',
@@ -37,7 +22,21 @@ describe('Top level exports', () => {
'log',
'DICOMWeb',
'DICOMSR',
- 'OHIF', //
+ 'OHIF',
+ //
+ 'CineService',
+ 'UIDialogService',
+ 'UIModalService',
+ 'UINotificationService',
+ 'UIViewportDialogService',
+ 'DisplaySetService',
+ 'MeasurementService',
+ 'ToolBarService',
+ 'ViewportGridService',
+ 'HangingProtocolService',
+ 'IWebApiDataSource',
+ 'DicomMetadataStore',
+ 'pubSubServiceInterface'
].sort();
const exports = Object.keys(OHIF).sort();
diff --git a/platform/core/src/measurements/tools/polygonRoi.js b/platform/core/src/measurements/tools/polygonRoi.js
new file mode 100644
index 000000000..4ebdc4d97
--- /dev/null
+++ b/platform/core/src/measurements/tools/polygonRoi.js
@@ -0,0 +1,24 @@
+const displayFunction = data => {
+ let meanValue = '';
+ const { cachedStats } = data;
+ if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
+ meanValue = cachedStats.mean.toFixed(2) + ' HU';
+ }
+ return meanValue;
+};
+
+export const polygonRoi = {
+ id: 'PolygonRoi',
+ name: 'Polygon',
+ toolGroup: 'allTools',
+ cornerstoneToolType: 'FreehandRoiTool',
+ options: {
+ measurementTable: {
+ displayFunction,
+ },
+ caseProgress: {
+ include: true,
+ evaluate: true,
+ },
+ },
+};
diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
index 6d5a353eb..3ee93ec85 100644
--- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
+++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
@@ -54,6 +54,18 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
);
}
+function _getInstanceFromImageId(imageId) {
+ for (let study of _model.studies) {
+ for (let series of study.series) {
+ for (let instance of series.instances) {
+ if (instance.imageId === imageId) {
+ return instance;
+ }
+ }
+ }
+ }
+}
+
const BaseImplementation = {
EVENTS,
listeners: {},
@@ -82,7 +94,7 @@ const BaseImplementation = {
madeInClient,
});
},
- addSeriesMetadata(seriesSummaryMetadata) {
+ addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) {
const { StudyInstanceUID } = seriesSummaryMetadata[0];
let study = _getStudy(StudyInstanceUID);
if (!study) {
@@ -98,6 +110,7 @@ const BaseImplementation = {
this._broadcastEvent(EVENTS.SERIES_ADDED, {
StudyInstanceUID,
+ madeInClient,
});
},
addStudy(study) {
@@ -124,6 +137,7 @@ const BaseImplementation = {
getStudy: _getStudy,
getSeries: _getSeries,
getInstance: _getInstance,
+ getInstanceFromImageId: _getInstanceFromImageId,
};
const DicomMetadataStore = Object.assign(
diff --git a/platform/core/src/services/DicomMetadataStore/EVENTS.js b/platform/core/src/services/DicomMetadataStore/EVENTS.js
index 7f30be511..09c1475da 100644
--- a/platform/core/src/services/DicomMetadataStore/EVENTS.js
+++ b/platform/core/src/services/DicomMetadataStore/EVENTS.js
@@ -1,4 +1,5 @@
const EVENTS = {
+ STUDY_ADDED: 'event::dicomMetadataStore:studyAdded',
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
};
diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js
index 8407e2d26..3f00dd3c8 100644
--- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js
+++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js
@@ -3,6 +3,7 @@ import createSeriesMetadata from './createSeriesMetadata';
function createStudyMetadata(StudyInstanceUID) {
return {
StudyInstanceUID,
+ isLoaded: false,
series: [],
/**
*
@@ -24,7 +25,7 @@ function createStudyMetadata(StudyInstanceUID) {
}
},
- setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) {
+ setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) {
let existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID
);
@@ -34,7 +35,7 @@ function createStudyMetadata(StudyInstanceUID) {
} else {
this.series.push(Object.assign({ instances: [] }, seriesMetadata));
}
- }
+ },
};
}
diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js
index 4229e37b0..9481559ad 100644
--- a/platform/core/src/services/DisplaySetService/DisplaySetService.js
+++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js
@@ -96,10 +96,13 @@ export default class DisplaySetService {
/**
*
* @param {*} input
- * @param {*} param1
+ * @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering
* @returns {string[]} - added displaySetInstanceUIDs
*/
- makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
+ makeDisplaySets = (
+ input,
+ { batch = false, madeInClient = false, settings = {} } = {}
+ ) => {
if (!input || !input.length) {
throw new Error('No instances were provided.');
}
@@ -116,12 +119,15 @@ export default class DisplaySetService {
if (batch) {
for (let i = 0; i < input.length; i++) {
const instances = input[i];
- const displaySets = this.makeDisplaySetForInstances(instances);
+ const displaySets = this.makeDisplaySetForInstances(
+ instances,
+ settings
+ );
displaySetsAdded = [...displaySetsAdded, displaySets];
}
} else {
- const displaySets = this.makeDisplaySetForInstances(input);
+ const displaySets = this.makeDisplaySetForInstances(input, settings);
displaySetsAdded = displaySets;
}
@@ -145,7 +151,7 @@ export default class DisplaySetService {
}
};
- makeDisplaySetForInstances(instances) {
+ makeDisplaySetForInstances(instances, settings) {
const instance = instances[0];
const existingDisplaySets =
@@ -168,6 +174,13 @@ export default class DisplaySetService {
} else {
displaySets = handler.getDisplaySetsFromSeries(instances);
+ // applying hp-defined viewport settings to the displaysets
+ displaySets.forEach(ds => {
+ Object.keys(settings).forEach(key => {
+ ds[key] = settings[key];
+ });
+ });
+
this._addDisplaySetsToCache(displaySets);
this._addActiveDisplaySets(displaySets);
}
diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js
new file mode 100644
index 000000000..29b0e4a1c
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js
@@ -0,0 +1,96 @@
+import validate from './lib/validator';
+
+
+/**
+ * Match a Metadata instance against rules using Validate.js for validation.
+ * @param {InstanceMetadata} metadataInstance Metadata instance object
+ * @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
+ * @return {Object} Matching Object with score and details (which rule passed or failed)
+ */
+const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
+ const options = {
+ format: 'grouped',
+ };
+
+ const details = {
+ passed: [],
+ failed: [],
+ };
+
+ let requiredFailed = false;
+ let score = 0;
+
+ rules.forEach(rule => {
+ const attribute = rule.attribute;
+
+ // Do not use the custom attribute from the metadataInstance since it is subject to change
+ if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
+ const customAttribute = customAttributeRetrievalCallbacks[attribute];
+ metadataInstance[attribute] = customAttribute.callback(metadataInstance);
+ }
+
+ // Format the constraint as required by Validate.js
+ const testConstraint = {
+ [attribute]: rule.constraint,
+ };
+
+ // Create a single attribute object to be validated, since metadataInstance is an
+ // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
+ const attributeValue = metadataInstance[attribute];
+ const attributeMap = {
+ [attribute]: attributeValue,
+ };
+
+ // Use Validate.js to evaluate the constraints on the specified metadataInstance
+ let errorMessages;
+ try {
+ errorMessages = validate(attributeMap, testConstraint, [options]);
+ } catch (e) {
+ errorMessages = ['Something went wrong during validation.', e];
+ }
+
+ if (!errorMessages) {
+ // If no errorMessages were returned, then validation passed.
+
+ // Add the rule's weight to the total score
+ score += parseInt(rule.weight, 10);
+
+ // Log that this rule passed in the matching details object
+ details.passed.push({
+ rule,
+ });
+ } else {
+ // If errorMessages were present, then validation failed
+
+ // If the rule that failed validation was Required, then
+ // mark that a required Rule has failed
+ if (rule.required) {
+ requiredFailed = true;
+ }
+
+ // Log that this rule failed in the matching details object
+ // and include any error messages
+ details.failed.push({
+ rule,
+ errorMessages,
+ });
+ }
+ });
+
+ // If a required Rule has failed Validation, set the matching score to zero
+ if (requiredFailed) {
+ score = 0;
+ }
+
+ return {
+ score,
+ details,
+ requiredFailed,
+ };
+};
+
+const HPMatcher = {
+ match,
+};
+
+export { HPMatcher };
diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js
index 1127c0a66..6d8af2189 100644
--- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js
+++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js
@@ -1,74 +1,476 @@
-const name = 'HangingProtocolService';
+import { cloneDeep } from 'lodash';
+import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
+import sortBy from '../../utils/sortBy.js';
+import ProtocolEngine from './ProtocolEngine';
-const publicAPI = {
- name,
- getState: _getState,
- setHangingProtocol: _setHangingProtocol,
- setHangingProtocolAppliedForViewport: _setHangingProtocolAppliedForViewport,
- setServiceImplementation,
- reset: _reset,
- set: _set,
+const EVENTS = {
+ STAGE_CHANGE: 'event::hanging_protocol_stage_change',
+ NEW_LAYOUT: 'event::hanging_protocol_new_layout',
};
-const serviceImplementation = {
- _getState: () => console.warn('getState() NOT IMPLEMENTED'),
- _setHangingProtocol: () =>
- console.warn('_setHangingProtocol() NOT IMPLEMENTED'),
- _setHangingProtocolAppliedForViewport: () =>
- console.warn('_setHangingProtocolAppliedForViewport() NOT IMPLEMENTED'),
- _reset: () => console.warn('reset() NOT IMPLEMENTED'),
- _set: () => console.warn('set() NOT IMPLEMENTED'),
+const VIEWPORT_SETTING_TYPES = {
+ PROPS: 'props',
+ VIEWPORT: 'viewport',
};
-function _getState() {
- return serviceImplementation._getState();
-}
-
-function _setHangingProtocol(hangingProtocol) {
- return serviceImplementation._setHangingProtocol(hangingProtocol);
-}
-
-function _setHangingProtocolAppliedForViewport(hpAlreadyApplied) {
- return serviceImplementation._setHangingProtocolAppliedForViewport(
- hpAlreadyApplied
- );
-}
-
-function _set(state) {
- return serviceImplementation._set(state);
-}
-
-function _reset() {
- return serviceImplementation._reset({});
-}
-
-function setServiceImplementation({
- getState: getStateImplementation,
- setHangingProtocol: setHangingProtocolImplementation,
- setHangingProtocolAppliedForViewport: setHangingProtocolAppliedForViewportImplementation,
- reset: resetImplementation,
- set: setImplementation,
-}) {
- if (getStateImplementation) {
- serviceImplementation._getState = getStateImplementation;
+class HangingProtocolService {
+ constructor(commandsManager) {
+ this._commandsManager = commandsManager;
+ this.protocols = [];
+ this.ProtocolEngine = undefined;
+ this.protocol = undefined;
+ this.stage = undefined;
+ this.matchDetails = [];
+ this.hpAlreadyApplied = [];
+ this.studies = [];
+ this.customViewportSettings = [];
+ this.customAttributeRetrievalCallbacks = {};
+ this.listeners = {};
+ Object.defineProperty(this, 'EVENTS', {
+ value: EVENTS,
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ });
+ Object.assign(this, pubSubServiceInterface);
}
- if (setHangingProtocolImplementation) {
- serviceImplementation._setHangingProtocol = setHangingProtocolImplementation;
+
+ reset() {
+ this.studies = [];
+ this.protocols = [];
+ this.hpAlreadyApplied = [];
+ this.matchDetails = [];
+ // this.ProtocolEngine.reset()
}
- if (setHangingProtocolAppliedForViewportImplementation) {
- serviceImplementation._setHangingProtocolAppliedForViewport = setHangingProtocolAppliedForViewportImplementation;
+
+ getState() {
+ return [this.matchDetails, this.hpAlreadyApplied];
}
- if (resetImplementation) {
- serviceImplementation._reset = resetImplementation;
+
+ getProtocols() {
+ return this.protocols;
}
- if (setImplementation) {
- serviceImplementation._set = setImplementation;
+
+ addProtocols(protocols) {
+ protocols.forEach(protocol => {
+ if (this.protocols.indexOf(protocol) === -1) {
+ this.protocols.push(protocol);
+ }
+ });
+ }
+
+ run(studyMetaData, protocol) {
+ if (!this.studies.includes(studyMetaData)) {
+ this.studies.push(studyMetaData);
+ }
+ // copy here so we don't mutate it
+ const metaData = Object.assign({}, studyMetaData);
+
+ this.ProtocolEngine = new ProtocolEngine(
+ this.protocols,
+ this.customAttributeRetrievalCallbacks
+ );
+
+ // if there is no pre-defiend protocol
+ if (!protocol || protocol.id === undefined) {
+ const matchedProtocol = this.ProtocolEngine.run(metaData);
+ this._setProtocol(matchedProtocol);
+ return;
+ }
+
+ this._setProtocol(protocol);
+ }
+
+ setHangingProtocolAppliedForViewport(i) {
+ this.hpAlreadyApplied[i] = true;
+ }
+
+ /**
+ * Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
+ *
+ * @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
+ * @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
+ * @param options
+ * @param callback A function to be run after a viewport is rendered with a series
+ */
+ addCustomViewportSetting(...params) {
+ this.customViewportSettings.push(...params);
+ }
+
+ /**
+ * Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
+ * callback that will be used to calculate the attribute value.
+ *
+ * @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
+ * @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
+ * @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
+ */
+ addCustomAttribute(attributeId, attributeName, callback) {
+ this.customAttributeRetrievalCallbacks[attributeId] = {
+ name: attributeName,
+ callback: callback,
+ };
+ }
+
+ /**
+ * Switches to the next protocol stage in the display set sequence
+ */
+ nextProtocolStage() {
+ console.log('ProtocolEngine::nextProtocolStage');
+
+ if (!this._setCurrentProtocolStage(1)) {
+ console.log('ProtocolEngine::nextProtocolStage failed');
+ }
+ }
+
+ /**
+ * Switches to the previous protocol stage in the display set sequence
+ */
+ previousProtocolStage() {
+ console.log('ProtocolEngine::previousProtocolStage');
+
+ if (!this._setCurrentProtocolStage(-1)) {
+ console.log('ProtocolEngine::previousProtocolStage failed');
+ }
+ }
+
+ _setProtocol(protocol) {
+ // TODO: Add proper Protocol class to validate the protocols
+ // which are entered manually
+ this.stage = 0;
+ this.protocol = protocol;
+ this._updateViewports(protocol);
+ }
+
+ /**
+ * Retrieves the number of Stages in the current Protocol or
+ * undefined if no protocol or stages are set
+ */
+ _getNumProtocolStages() {
+ if (
+ !this.protocol ||
+ !this.protocol.stages ||
+ !this.protocol.stages.length
+ ) {
+ return;
+ }
+
+ return this.protocol.stages.length;
+ }
+
+ /**
+ * Retrieves the current Stage from the current Protocol and stage index
+ *
+ * @returns {*} The Stage model for the currently displayed Stage
+ */
+ _getCurrentStageModel() {
+ return this.protocol.stages[this.stage];
+ }
+
+ _updateViewports() {
+ // Make sure we have an active protocol with a non-empty array of display sets
+ if (!this._getNumProtocolStages()) {
+ return;
+ }
+
+ // Retrieve the current stage
+ const stageModel = this._getCurrentStageModel();
+
+ // If the current stage does not fulfill the requirements to be displayed,
+ // stop here.
+ if (
+ !stageModel ||
+ !stageModel.viewportStructure ||
+ !stageModel.viewports ||
+ !stageModel.viewports.length
+ ) {
+ return;
+ }
+
+ // Retrieve the layoutTemplate associated with the current display set's viewport structure
+ // If no such template name exists, stop here.
+ // const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
+ const layoutTemplateName = 'gridLayout';
+ if (!layoutTemplateName) {
+ return;
+ }
+
+ // Retrieve the properties associated with the current display set's viewport structure template
+ // If no such layout properties exist, stop here.
+ const layoutProps = stageModel.viewportStructure.properties;
+ if (!layoutProps) {
+ return;
+ }
+
+ const { columns: numCols, rows: numRows } = layoutProps;
+ this._broadcastChange(this.EVENTS.NEW_LAYOUT, {
+ numRows,
+ numCols,
+ });
+
+ // Empty the matchDetails associated with the ProtocolEngine.
+ // This will be used to store the pass/fail details and score
+ // for each of the viewport matching procedures
+
+ // Loop through each viewport
+ stageModel.viewports.forEach((viewport, viewportIndex) => {
+ this.hpAlreadyApplied.push(false);
+ const details = this._matchImages(viewport);
+
+ let currentMatch = details.bestMatch;
+
+ const currentViewportData = {
+ viewportIndex,
+ SeriesInstanceUID: currentMatch && currentMatch.SeriesInstanceUID,
+ };
+
+ // Viewport Settings
+ //
+ // protocol defined callback
+ const protocolCallbacks = viewport.viewportSettings.filter(
+ setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
+ );
+ // manually added callback
+ const customCallbacks = this.customViewportSettings.filter(
+ setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
+ );
+ const callbacks = protocolCallbacks.concat(customCallbacks);
+
+ // if we have callbacks to applied at the app level or at the HP level
+ if (callbacks.length) {
+ currentViewportData.renderedCallback = (element, ToolBarService) => {
+ callbacks.forEach(setting => {
+ const { commandName, options } = setting;
+ options.viewportIndex = viewportIndex;
+ options.element = element;
+ // Toolbar service to handle tool activation
+ if (commandName === 'setToolActive') {
+ ToolBarService.recordInteraction(options);
+ return;
+ }
+ // other commands
+ this._commandsManager.runCommand(commandName, options);
+ });
+ };
+ }
+
+ // initial viewport settings defined by protocol
+ const protocolInitialViewport = viewport.viewportSettings.filter(
+ setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
+ );
+ // custom added initial viewport settings
+ const customInitialViewport = this.customViewportSettings.filter(
+ setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
+ );
+ // TODO: conflict might happen between protocol and custom viewport settings
+ const viewportSettings = protocolInitialViewport.concat(
+ customInitialViewport
+ );
+
+ if (viewportSettings.length) {
+ const initialViewport = {};
+ viewportSettings.forEach(setting => {
+ const { options } = setting;
+ if (!options) return;
+ // Do not manipulate the hp settings
+ const viewportOptions = cloneDeep(options);
+ Object.entries(viewportOptions).forEach(([key, value]) => {
+ initialViewport[key] = value;
+ });
+ });
+ currentViewportData.initialViewport = initialViewport;
+ }
+
+ this.matchDetails[viewportIndex] = currentViewportData;
+ });
+ }
+
+ // Match images given a list of Studies and a Viewport's image matching reqs
+ _matchImages(viewport) {
+ console.log('ProtocolEngine::matchImages');
+
+ // TODO: matching is applied on study and series level, instance
+ // level matching needs to be added in future
+
+ const { studyMatchingRules, seriesMatchingRules } = viewport;
+
+ const matchingScores = [];
+ let highestStudyMatchingScore = 0;
+ let highestSeriesMatchingScore = 0;
+
+ this.studies.forEach(study => {
+ const studyMatchDetails = this.ProtocolEngine.findMatch(
+ study,
+ studyMatchingRules
+ );
+
+ // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
+ if (
+ studyMatchDetails.requiredFailed === true ||
+ studyMatchDetails.score < highestStudyMatchingScore
+ ) {
+ return;
+ }
+
+ highestStudyMatchingScore = studyMatchDetails.score;
+
+ study.series.forEach(aSeries => {
+ const seriesMatchDetails = this.ProtocolEngine.findMatch(
+ aSeries,
+ seriesMatchingRules
+ );
+
+ // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
+ if (
+ seriesMatchDetails.requiredFailed === true ||
+ seriesMatchDetails.score < highestSeriesMatchingScore
+ ) {
+ return;
+ }
+
+ highestSeriesMatchingScore = seriesMatchDetails.score;
+
+ const matchDetails = {
+ passed: [],
+ failed: [],
+ };
+
+ matchDetails.passed = matchDetails.passed.concat(
+ seriesMatchDetails.details.passed
+ );
+ matchDetails.passed = matchDetails.passed.concat(
+ studyMatchDetails.details.passed
+ );
+
+ matchDetails.failed = matchDetails.failed.concat(
+ seriesMatchDetails.details.failed
+ );
+ matchDetails.failed = matchDetails.failed.concat(
+ studyMatchDetails.details.failed
+ );
+
+ const totalMatchScore =
+ seriesMatchDetails.score + studyMatchDetails.score;
+
+ const imageDetails = {
+ StudyInstanceUID: study.StudyInstanceUID,
+ SeriesInstanceUID: aSeries.SeriesInstanceUID,
+ matchingScore: totalMatchScore,
+ matchDetails: matchDetails,
+ sortingInfo: {
+ score: totalMatchScore,
+ study: study.StudyInstanceUID,
+ series: parseInt(aSeries.SeriesNumber),
+ },
+ };
+
+ matchingScores.push(imageDetails);
+ });
+ });
+
+ // Sort the matchingScores
+ const sortingFunction = sortBy(
+ {
+ name: 'score',
+ reverse: true,
+ },
+ {
+ name: 'study',
+ reverse: true,
+ },
+ {
+ name: 'series',
+ }
+ );
+ matchingScores.sort((a, b) =>
+ sortingFunction(a.sortingInfo, b.sortingInfo)
+ );
+
+ const bestMatch = matchingScores[0];
+
+ console.log('ProtocolEngine::matchImages bestMatch', bestMatch);
+
+ return {
+ bestMatch,
+ matchingScores,
+ };
+ }
+
+ /**
+ * Check if the next stage is available
+ * @return {Boolean} True if next stage is available or false otherwise
+ */
+ _isNextStageAvailable() {
+ const numberOfStages = this._getNumProtocolStages();
+
+ return this.stage + 1 < numberOfStages;
+ }
+
+ /**
+ * Check if the previous stage is available
+ * @return {Boolean} True if previous stage is available or false otherwise
+ */
+ _isPreviousStageAvailable() {
+ return this.stage - 1 >= 0;
+ }
+
+ /**
+ * Changes the current stage to a new stage index in the display set sequence.
+ * It checks if the next stage exists.
+ *
+ * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
+ * @return {Boolean} True if new stage has set or false, otherwise
+ */
+ _setCurrentProtocolStage(stageAction) {
+ //reseting the applied protocols
+ this.hpAlreadyApplied = [];
+ // Check if previous or next stage is available
+ if (stageAction === -1 && !this._isPreviousStageAvailable()) {
+ return false;
+ } else if (stageAction === 1 && !this._isNextStageAvailable()) {
+ return false;
+ }
+
+ // Sets the new stage
+ this.stage += stageAction;
+
+ // Log the new stage
+ console.log(
+ `ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`
+ );
+
+ // Since stage has changed, we need to update the viewports
+ // and redo matchings
+ this._updateViewports();
+
+ // Everything went well
+ this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
+ matchDetails: this.matchDetails,
+ hpAlreadyApplied: this.hpAlreadyApplied,
+ });
+ return true;
+ }
+ /**
+ * Broadcasts hanging protocols changes.
+ *
+ * @param {string} eventName The event name.add
+ * @param {object} eventData.source The measurement source.
+ * @param {object} eventData.measurement The measurement.
+ * @param {boolean} eventData.notYetUpdatedAtSource True if the measurement was edited
+ * within the measurement service and the source needs to update.
+ * @return void
+ */
+ _broadcastChange(eventName, eventData) {
+ const hasListeners = Object.keys(this.listeners).length > 0;
+ const hasCallbacks = Array.isArray(this.listeners[eventName]);
+
+ if (hasListeners && hasCallbacks) {
+ this.listeners[eventName].forEach(listener => {
+ listener.callback(eventData);
+ });
+ }
}
}
-export default {
- name,
- create: ({ configuration = {} }) => {
- return publicAPI;
- },
-};
+export default HangingProtocolService;
+export { EVENTS };
diff --git a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js
new file mode 100644
index 000000000..32d5c253c
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js
@@ -0,0 +1,789 @@
+import { HPMatcher } from './HPMatcher.js';
+import { sortByScore } from './lib/sortByScore';
+
+const deafultProtocol = {
+ id: 'defaultProtocol',
+ locked: true,
+ hasUpdatedPriorsInformation: false,
+ name: 'Default',
+ createdDate: '2021-02-23T19:22:08.894Z',
+ modifiedDate: '2021-02-23T19:22:08.894Z',
+ availableTo: {},
+ editableBy: {},
+ protocolMatchingRules: [],
+ stages: [
+ {
+ id: 'nwzau7jDkEkL8djfr',
+ name: 'oneByOne',
+ viewportStructure: {
+ type: 'grid',
+ properties: {
+ rows: 1,
+ columns: 1,
+ },
+ },
+ viewports: [
+ {
+ viewportSettings: [],
+ imageMatchingRules: [],
+ seriesMatchingRules: [],
+ studyMatchingRules: [],
+ },
+ ],
+ createdDate: '2021-02-23T19:22:08.894Z',
+ },
+ ],
+ numberOfPriorsReferenced: -1,
+};
+
+export default class ProtocolEngine {
+ constructor(protocols, customAttributeRetrievalCallbacks) {
+ this.protocols = protocols;
+ this.customAttributeRetrievalCallbacks = customAttributeRetrievalCallbacks;
+ this.matchedProtocols = new Map();
+ this.matchedProtocolScores = {};
+ this.study = undefined;
+ }
+
+ run(studyMetaData) {
+ this.study = studyMetaData;
+ return this.getBestProtocolMatch();
+ }
+
+ // /**
+ // * Resets the ProtocolEngine to the best match
+ // */
+ // reset() {
+ // const protocol = this.getBestProtocolMatch();
+
+ // this.setHangingProtocol(protocol);
+ // }
+
+ /**
+ * Return the best matched Protocol to the current study or set of studies
+ * @returns {*}
+ */
+ getBestProtocolMatch() {
+ // Run the matching to populate matchedProtocols Set and Map
+ this.updateProtocolMatches();
+
+ // Retrieve the highest scoring Protocol
+ const bestMatch = this._getHighestScoringProtocol();
+
+ console.log('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
+
+ return bestMatch;
+ }
+
+ /**
+ * Populates the MatchedProtocols Collection by running the matching procedure
+ */
+ updateProtocolMatches() {
+ console.log('ProtocolEngine::updateProtocolMatches');
+
+ // Clear all data currently in matchedProtocols
+ this._clearMatchedProtocols();
+
+ // TODO: handle more than one study
+ const study = this.study;
+ const matched = this.findMatchByStudy(study);
+
+ // For each matched protocol, check if it is already in MatchedProtocols
+ matched.forEach(matchedDetail => {
+ const protocol = matchedDetail.protocol;
+ if (!protocol) {
+ return;
+ }
+
+ // If it is not already in the MatchedProtocols Collection, insert it with its score
+ if (!this.matchedProtocols.has(protocol.id)) {
+ console.log(
+ 'ProtocolEngine::updateProtocolMatches inserting protocol match',
+ matchedDetail
+ );
+ this.matchedProtocols.set(protocol.id, protocol);
+ this.matchedProtocolScores[protocol.id] = matchedDetail.score;
+ }
+ });
+ }
+
+ findMatch(metaData, rules) {
+ return HPMatcher.match(
+ metaData,
+ rules,
+ this.customAttributeRetrievalCallbacks
+ );
+ }
+
+ /**
+ * Finds the best protocols from Protocol Store, matching each protocol matching rules
+ * with the given study. The best protocol are orded by score and returned in an array
+ * @param {Object} study StudyMetadata instance object
+ * @return {Array} Array of match objects or an empty array if no match was found
+ * Each match object has the score of the matching and the matched
+ * protocol
+ */
+ findMatchByStudy(study) {
+ const matched = [];
+
+ this.protocols.forEach(protocol => {
+ // Clone the protocol's protocolMatchingRules array
+ // We clone it so that we don't accidentally add the
+ // numberOfPriorsReferenced rule to the Protocol itself.
+ let rules = protocol.protocolMatchingRules.slice();
+ if (!rules) {
+ return;
+ }
+
+ // Run the matcher and get matching details
+ const matchedDetails = this.findMatch(study, rules);
+ const score = matchedDetails.score;
+
+ // The protocol matched some rule, add it to the matched list
+ if (score > 0) {
+ matched.push({
+ score,
+ protocol,
+ });
+ }
+ });
+
+ // If no matches were found, select the default protocol
+ if (!matched.length) {
+ return [
+ {
+ score: 1,
+ protocol: deafultProtocol,
+ },
+ ];
+ }
+
+ // Sort the matched list by score
+ sortByScore(matched);
+
+ console.log('ProtocolEngine::findMatchByStudy matched', matched);
+
+ return matched;
+ }
+
+ _clearMatchedProtocols() {
+ this.matchedProtocols.clear();
+ this.matchedProtocolScores = {};
+ }
+
+ _largestKeyByValue(obj) {
+ return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
+ }
+
+ _getHighestScoringProtocol() {
+ if (!Object.keys(this.matchedProtocolScores).length) {
+ return;
+ }
+ const highestScoringProtocolId = this._largestKeyByValue(
+ this.matchedProtocolScores
+ );
+ return this.matchedProtocols.get(highestScoringProtocolId);
+ }
+}
+
+/**
+ * Resets the ProtocolEngine to the best match
+ */
+
+/**
+ * Retrieves the current Stage from the current Protocol and stage index
+ *
+ * @returns {*} The Stage model for the currently displayed Stage
+ */
+// getCurrentStageModel() {
+// return this.protocol.stages[this.stage];
+// }
+
+// /**
+// * Get the number of prior studies supplied in the priorStudies map property.
+// *
+// * @param {String} studyObjectID The study object ID of the study whose priors are needed
+// * @returns {number} The number of available prior studies with the same PatientID
+// */
+// getNumberOfAvailablePriors(studyObjectID) {
+// return this.getAvailableStudyPriors(studyObjectID).length;
+// }
+
+// /**
+// * Get the array of prior studies from a specific study.
+// *
+// * @param {String} studyObjectID The study object ID of the study whose priors are needed
+// * @returns {Array} The array of available priors or an empty array
+// */
+// getAvailableStudyPriors(studyObjectID) {
+// const priors = this.priorStudies.get(studyObjectID);
+
+// return priors instanceof Array ? priors : [];
+// }
+
+// // Match images given a list of Studies and a Viewport's image matching reqs
+// matchImages(viewport, viewportIndex) {
+// log.trace('ProtocolEngine::matchImages');
+
+// const {
+// studyMatchingRules,
+// seriesMatchingRules,
+// imageMatchingRules: instanceMatchingRules,
+// } = viewport;
+
+// const matchingScores = [];
+// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
+// const firstInstance = currentStudy.getFirstInstance();
+
+// let highestStudyMatchingScore = 0;
+// let highestSeriesMatchingScore = 0;
+
+// // Set custom attribute for study metadata and it's first instance
+// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
+// if (firstInstance instanceof InstanceMetadata) {
+// firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
+// }
+
+// // Only used if study matching rules has abstract prior values defined...
+// let priorStudies;
+
+// studyMatchingRules.forEach(rule => {
+// if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
+// const validatorType = Object.keys(rule.constraint)[0];
+// const validator = Object.keys(rule.constraint[validatorType])[0];
+
+// let abstractPriorValue = rule.constraint[validatorType][validator];
+// abstractPriorValue = parseInt(abstractPriorValue, 10);
+// // TODO: Restrict or clarify validators for abstractPriorValue?
+
+// // No need to call it more than once...
+// if (!priorStudies) {
+// priorStudies = this.getAvailableStudyPriors(
+// currentStudy.getObjectID()
+// );
+// }
+
+// // TODO: Revisit this later: What about two studies with the same
+// // study date?
+
+// let priorStudy;
+// if (abstractPriorValue === -1) {
+// priorStudy = priorStudies[priorStudies.length - 1];
+// } else {
+// const studyIndex = Math.max(abstractPriorValue - 1, 0);
+// priorStudy = priorStudies[studyIndex];
+// }
+
+// // Invalid data
+// if (!priorStudy instanceof StudyMetadata) {
+// return;
+// }
+
+// const priorStudyObjectID = priorStudy.getObjectID();
+
+// // Check if study metadata is already in studies list
+// if (
+// this.studies.find(study => study.getObjectID() === priorStudyObjectID)
+// ) {
+// return;
+// }
+
+// // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
+// this.studyMetadataSource.loadStudy(priorStudy).then(
+// studyMetadata => {
+// // Set the custom attribute abstractPriorValue for the study metadata
+// studyMetadata.setCustomAttribute(
+// ABSTRACT_PRIOR_VALUE,
+// abstractPriorValue
+// );
+
+// // Also add custom attribute
+// const firstInstance = studyMetadata.getFirstInstance();
+// if (firstInstance instanceof InstanceMetadata) {
+// firstInstance.setCustomAttribute(
+// ABSTRACT_PRIOR_VALUE,
+// abstractPriorValue
+// );
+// }
+
+// // Insert the new study metadata
+// this.studies.push(studyMetadata);
+
+// // Update the viewport to refresh layout manager with new study
+// this.updateViewports(viewportIndex);
+// },
+// error => {
+// log.warn(error);
+// throw new OHIFError(
+// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
+// );
+// }
+// );
+// }
+// // TODO: Add relative Date / time
+// });
+
+// this.studies.forEach(study => {
+// const studyMatchDetails = HPMatcher.match(
+// study.getFirstInstance(),
+// studyMatchingRules
+// );
+
+// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
+// if (
+// studyMatchDetails.requiredFailed === true ||
+// studyMatchDetails.score < highestStudyMatchingScore
+// ) {
+// return;
+// }
+
+// highestStudyMatchingScore = studyMatchDetails.score;
+
+// study.forEachSeries(series => {
+// const seriesMatchDetails = HPMatcher.match(
+// series.getFirstInstance(),
+// seriesMatchingRules
+// );
+
+// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
+// if (
+// seriesMatchDetails.requiredFailed === true ||
+// seriesMatchDetails.score < highestSeriesMatchingScore
+// ) {
+// return;
+// }
+
+// highestSeriesMatchingScore = seriesMatchDetails.score;
+
+// series.forEachInstance((instance, index) => {
+// // This tests to make sure there is actually image data in this instance
+// // TODO: Change this when we add PDF and MPEG support
+// // See https://ohiforg.atlassian.net/browse/LT-227
+// if (
+// !isImage(instance.getTagValue('SOPClassUID')) &&
+// !instance.getTagValue('Rows')
+// ) {
+// return;
+// }
+
+// const instanceMatchDetails = HPMatcher.match(
+// instance,
+// instanceMatchingRules
+// );
+
+// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
+// if (instanceMatchDetails.requiredFailed === true) {
+// return;
+// }
+
+// const matchDetails = {
+// passed: [],
+// failed: [],
+// };
+
+// matchDetails.passed = matchDetails.passed.concat(
+// instanceMatchDetails.details.passed
+// );
+// matchDetails.passed = matchDetails.passed.concat(
+// seriesMatchDetails.details.passed
+// );
+// matchDetails.passed = matchDetails.passed.concat(
+// studyMatchDetails.details.passed
+// );
+
+// matchDetails.failed = matchDetails.failed.concat(
+// instanceMatchDetails.details.failed
+// );
+// matchDetails.failed = matchDetails.failed.concat(
+// seriesMatchDetails.details.failed
+// );
+// matchDetails.failed = matchDetails.failed.concat(
+// studyMatchDetails.details.failed
+// );
+
+// const totalMatchScore =
+// instanceMatchDetails.score +
+// seriesMatchDetails.score +
+// studyMatchDetails.score;
+// const currentSOPInstanceUID = instance.getSOPInstanceUID();
+
+// const imageDetails = {
+// StudyInstanceUID: study.getStudyInstanceUID(),
+// SeriesInstanceUID: series.getSeriesInstanceUID(),
+// SOPInstanceUID: currentSOPInstanceUID,
+// currentImageIdIndex: index,
+// matchingScore: totalMatchScore,
+// matchDetails: matchDetails,
+// sortingInfo: {
+// score: totalMatchScore,
+// study:
+// instance.getTagValue('StudyDate') +
+// instance.getTagValue('StudyTime'),
+// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
+// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
+// },
+// };
+
+// // Find the displaySet
+// const displaySet = study.findDisplaySet(displaySet =>
+// displaySet.images.find(
+// image => image.getSOPInstanceUID() === currentSOPInstanceUID
+// )
+// );
+
+// // If the instance was found, set the displaySet ID
+// if (displaySet) {
+// imageDetails.displaySetInstanceUID = displaySet.getUID();
+// imageDetails.imageId = instance.getImageId();
+// }
+
+// matchingScores.push(imageDetails);
+// });
+// });
+// });
+
+// // Sort the matchingScores
+// const sortingFunction = sortBy(
+// {
+// name: 'score',
+// reverse: true,
+// },
+// {
+// name: 'study',
+// reverse: true,
+// },
+// {
+// name: 'instance',
+// },
+// {
+// name: 'series',
+// }
+// );
+// matchingScores.sort((a, b) =>
+// sortingFunction(a.sortingInfo, b.sortingInfo)
+// );
+
+// const bestMatch = matchingScores[0];
+
+// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
+
+// return {
+// bestMatch,
+// matchingScores,
+// };
+// }
+
+/**
+ * Sets the current layout
+ *
+ * @param {number} numRows
+ * @param {number} numColumns
+ */
+// setLayout(numRows, numColumns) {
+// if (numRows < 1 && numColumns < 1) {
+// log.error(`Invalid layout ${numRows} x ${numColumns}`);
+// return;
+// }
+
+// if (typeof this.options.setLayout !== 'function') {
+// log.error('Hanging Protocol Engine setLayout callback is not defined');
+// return;
+// }
+
+// let viewports = [];
+// const numViewports = numRows * numColumns;
+
+// for (let i = 0; i < numViewports; i++) {
+// viewports.push({});
+// }
+
+// this.options.setLayout({ numRows, numColumns, viewports });
+// }
+
+// /**
+// * Rerenders viewports that are part of the current layout manager
+// * using the matching rules internal to each viewport.
+// *
+// * If this function is provided the index of a viewport, only the specified viewport
+// * is rerendered.
+// *
+// * @param viewportIndex
+// */
+// updateViewports(viewportIndex) {
+// log.trace(
+// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
+// );
+
+// // Make sure we have an active protocol with a non-empty array of display sets
+// if (!this.getNumProtocolStages()) {
+// return;
+// }
+
+// // Retrieve the current stage
+// const stageModel = this.getCurrentStageModel();
+
+// // If the current stage does not fulfill the requirements to be displayed,
+// // stop here.
+// if (
+// !stageModel ||
+// !stageModel.viewportStructure ||
+// !stageModel.viewports ||
+// !stageModel.viewports.length
+// ) {
+// return;
+// }
+
+// // Retrieve the layoutTemplate associated with the current display set's viewport structure
+// // If no such template name exists, stop here.
+// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
+// if (!layoutTemplateName) {
+// return;
+// }
+
+// // Retrieve the properties associated with the current display set's viewport structure template
+// // If no such layout properties exist, stop here.
+// const layoutProps = stageModel.viewportStructure.properties;
+// if (!layoutProps) {
+// return;
+// }
+
+// // Create an empty array to store the output viewportData
+// const viewportData = [];
+
+// // Empty the matchDetails associated with the ProtocolEngine.
+// // This will be used to store the pass/fail details and score
+// // for each of the viewport matching procedures
+// this.matchDetails = [];
+
+// // Loop through each viewport
+// stageModel.viewports.forEach((viewport, viewportIndex) => {
+// const details = this.matchImages(viewport, viewportIndex);
+
+// this.matchDetails[viewportIndex] = details;
+
+// // Convert any YES/NO values into true/false for Cornerstone
+// const cornerstoneViewportParams = {};
+
+// // Cache viewportSettings keys
+// const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
+
+// viewportSettingsKeys.forEach(key => {
+// let value = viewport.viewportSettings[key];
+// if (value === 'YES') {
+// value = true;
+// } else if (value === 'NO') {
+// value = false;
+// }
+
+// cornerstoneViewportParams[key] = value;
+// });
+
+// // imageViewerViewports occasionally needs relevant layout data in order to set
+// // the element style of the viewport in question
+// const currentViewportData = {
+// viewportIndex,
+// viewport: cornerstoneViewportParams,
+// ...layoutProps,
+// };
+
+// const customSettings = [];
+// viewportSettingsKeys.forEach(id => {
+// const setting = CustomViewportSettings[id];
+// if (!setting) {
+// return;
+// }
+
+// customSettings.push({
+// id: id,
+// value: viewport.viewportSettings[id],
+// });
+// });
+
+// currentViewportData.renderedCallback = element => {
+// //console.log('renderedCallback for ' + element.id);
+// customSettings.forEach(customSetting => {
+// log.trace(
+// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
+// );
+// log.trace(
+// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
+// );
+
+// const setting = CustomViewportSettings[customSetting.id];
+// setting.callback(element, customSetting.value);
+// });
+// };
+
+// let currentMatch = details.bestMatch;
+// let currentPosition = 1;
+// const scoresLength = details.matchingScores.length;
+// while (
+// currentPosition < scoresLength &&
+// viewportData.find(a => a.imageId === currentMatch.imageId)
+// ) {
+// currentMatch = details.matchingScores[currentPosition];
+// currentPosition++;
+// }
+
+// if (currentMatch && currentMatch.imageId) {
+// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
+// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
+// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
+// currentViewportData.currentImageIdIndex =
+// currentMatch.currentImageIdIndex;
+// currentViewportData.displaySetInstanceUID =
+// currentMatch.displaySetInstanceUID;
+// currentViewportData.imageId = currentMatch.imageId;
+// }
+
+// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
+// // if (!currentViewportData.displaySetInstanceUID) {
+// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
+// // }
+
+// viewportData.push(currentViewportData);
+// });
+
+// this.setLayout(layoutProps.Rows, layoutProps.Columns);
+
+// if (typeof this.options.setViewportSpecificData !== 'function') {
+// log.error(
+// 'Hanging Protocol Engine setViewportSpecificData callback is not defined'
+// );
+// return;
+// }
+
+// // If viewportIndex is defined, then update only that viewport
+// if (viewportIndex !== undefined && viewportData[viewportIndex]) {
+// this.options.setViewportSpecificData(
+// viewportIndex,
+// viewportData[viewportIndex]
+// );
+// return;
+// }
+
+// // Update all viewports
+// viewportData.forEach(viewportSpecificData => {
+// this.options.setViewportSpecificData(
+// viewportSpecificData.viewportIndex,
+// viewportSpecificData
+// );
+// });
+// }
+
+// /**
+// * Sets the current Hanging Protocol to the specified Protocol
+// * An optional argument can also be used to prevent the updating of the Viewports
+// *
+// * @param newProtocol
+// * @param updateViewports
+// */
+// setHangingProtocol(newProtocol, updateViewports = true) {
+// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
+// log.trace(
+// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
+// );
+
+// // Reset the array of newStageIds
+// this.newStageIds = [];
+
+// if (Protocol.prototype.isPrototypeOf(newProtocol)) {
+// this.protocol = newProtocol;
+// } else {
+// this.protocol = new Protocol();
+// this.protocol.fromObject(newProtocol);
+// }
+
+// this.stage = 0;
+
+// // Update viewports by default
+// if (updateViewports) {
+// this.updateViewports();
+// }
+// }
+
+// /**
+// * Check if the next stage is available
+// * @return {Boolean} True if next stage is available or false otherwise
+// */
+// isNextStageAvailable() {
+// const numberOfStages = this.getNumProtocolStages();
+
+// return this.stage + 1 < numberOfStages;
+// }
+
+// /**
+// * Check if the previous stage is available
+// * @return {Boolean} True if previous stage is available or false otherwise
+// */
+// isPreviousStageAvailable() {
+// return this.stage - 1 >= 0;
+// }
+
+// /**
+// * Changes the current stage to a new stage index in the display set sequence.
+// * It checks if the next stage exists.
+// *
+// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
+// * @return {Boolean} True if new stage has set or false, otherwise
+// */
+// setCurrentProtocolStage(stageAction) {
+// // Check if previous or next stage is available
+// if (stageAction === -1 && !this.isPreviousStageAvailable()) {
+// return false;
+// } else if (stageAction === 1 && !this.isNextStageAvailable()) {
+// return false;
+// }
+
+// // Sets the new stage
+// this.stage += stageAction;
+
+// // Log the new stage
+// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
+
+// // Since stage has changed, we need to update the viewports
+// // and redo matchings
+// this.updateViewports();
+
+// // Everything went well
+// return true;
+// }
+
+// /**
+// * Retrieves the number of Stages in the current Protocol or
+// * undefined if no protocol or stages are set
+// */
+// getNumProtocolStages() {
+// if (
+// !this.protocol ||
+// !this.protocol.stages ||
+// !this.protocol.stages.length
+// ) {
+// return;
+// }
+
+// return this.protocol.stages.length;
+// }
+
+// /**
+// * Switches to the next protocol stage in the display set sequence
+// */
+// nextProtocolStage() {
+// log.trace('ProtocolEngine::nextProtocolStage');
+
+// if (!this.setCurrentProtocolStage(1)) {
+// log.trace('ProtocolEngine::nextProtocolStage failed');
+// }
+// }
+
+// /**
+// * Switches to the previous protocol stage in the display set sequence
+// */
+// previousProtocolStage() {
+// log.trace('ProtocolEngine::previousProtocolStage');
+
+// if (!this.setCurrentProtocolStage(-1)) {
+// log.trace('ProtocolEngine::previousProtocolStage failed');
+// }
+// }
+// }
diff --git a/platform/core/src/services/HangingProtocolService/index.js b/platform/core/src/services/HangingProtocolService/index.js
index 2d5a41716..db50eaee3 100644
--- a/platform/core/src/services/HangingProtocolService/index.js
+++ b/platform/core/src/services/HangingProtocolService/index.js
@@ -1,3 +1,8 @@
import HangingProtocolService from './HangingProtocolService';
-export default HangingProtocolService;
+export default {
+ name: 'HangingProtocolService',
+ create: ({ configuration = {}, commandsManager }) => {
+ return new HangingProtocolService(commandsManager);
+ },
+};
diff --git a/platform/core/src/services/HangingProtocolService/lib/comparators.js b/platform/core/src/services/HangingProtocolService/lib/comparators.js
new file mode 100644
index 000000000..ce76a4d82
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/lib/comparators.js
@@ -0,0 +1,98 @@
+const comparators = [
+ {
+ id: 'equals',
+ name: '= (Equals)',
+ validator: 'equals',
+ validatorOption: 'value',
+ description: 'The attribute must equal this value.',
+ },
+ {
+ id: 'doesNotEqual',
+ name: '!= (Does not equal)',
+ validator: 'doesNotEqual',
+ validatorOption: 'value',
+ description: 'The attribute must not equal this value.',
+ },
+ {
+ id: 'contains',
+ name: 'Contains',
+ validator: 'contains',
+ validatorOption: 'value',
+ description: 'The attribute must contain this value.',
+ },
+ {
+ id: 'doesNotContain',
+ name: 'Does not contain',
+ validator: 'doesNotContain',
+ validatorOption: 'value',
+ description: 'The attribute must not contain this value.',
+ },
+ {
+ id: 'startsWith',
+ name: 'Starts with',
+ validator: 'startsWith',
+ validatorOption: 'value',
+ description: 'The attribute must start with this value.',
+ },
+ {
+ id: 'endsWith',
+ name: 'Ends with',
+ validator: 'endsWith',
+ validatorOption: 'value',
+ description: 'The attribute must end with this value.',
+ },
+ {
+ id: 'onlyInteger',
+ name: 'Only Integers',
+ validator: 'numericality',
+ validatorOption: 'onlyInteger',
+ description: "Real numbers won't be allowed.",
+ },
+ {
+ id: 'greaterThan',
+ name: '> (Greater than)',
+ validator: 'numericality',
+ validatorOption: 'greaterThan',
+ description: 'The attribute has to be greater than this value.',
+ },
+ {
+ id: 'greaterThanOrEqualTo',
+ name: '>= (Greater than or equal to)',
+ validator: 'numericality',
+ validatorOption: 'greaterThanOrEqualTo',
+ description: 'The attribute has to be at least this value.',
+ },
+ {
+ id: 'lessThanOrEqualTo',
+ name: '<= (Less than or equal to)',
+ validator: 'numericality',
+ validatorOption: 'lessThanOrEqualTo',
+ description: 'The attribute can be this value at the most.',
+ },
+ {
+ id: 'lessThan',
+ name: '< (Less than)',
+ validator: 'numericality',
+ validatorOption: 'lessThan',
+ description: 'The attribute has to be less than this value.',
+ },
+ {
+ id: 'odd',
+ name: 'Odd',
+ validator: 'numericality',
+ validatorOption: 'odd',
+ description: 'The attribute has to be odd.',
+ },
+ {
+ id: 'even',
+ name: 'Even',
+ validator: 'numericality',
+ validatorOption: 'even',
+ description: 'The attribute has to be even.',
+ },
+];
+
+// Immutable object
+Object.freeze(comparators);
+
+export { comparators };
diff --git a/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js b/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js
new file mode 100644
index 000000000..17368c877
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js
@@ -0,0 +1,71 @@
+const attributeCache = Object.create(null);
+const REGEXP = /^\([x0-9a-f]+\)/;
+
+const humanize = text => {
+ let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
+
+ humanized = humanized.replace(/^./, str => {
+ // uppercase the first character
+ return str.toUpperCase();
+ });
+
+ return humanized;
+};
+
+/**
+ * Get the text of an attribute for a given attribute
+ * @param {String} attributeId The attribute ID
+ * @param {Array} attributes Array of attributes objects with id and text properties
+ * @return {String} If found return the attribute text or an empty string otherwise
+ */
+const getAttributeText = (attributeId, attributes) => {
+ // If the attribute is already in the cache, return it
+ if (attributeId in attributeCache) {
+ return attributeCache[attributeId];
+ }
+
+ // Find the attribute with given attributeId
+ const attribute = attributes.find(attribute => attribute.id === attributeId);
+
+ let attributeText;
+
+ // If attribute was found get its text and save it on the cache
+ if (attribute) {
+ attributeText = attribute.text.replace(REGEXP, '');
+ attributeCache[attributeId] = attributeText;
+ }
+
+ return attributeText || '';
+};
+
+function displayConstraint(attributeId, constraint, attributes) {
+ if (!constraint || !attributeId) {
+ return;
+ }
+
+ const validatorType = Object.keys(constraint)[0];
+ if (!validatorType) {
+ return;
+ }
+
+ const validator = Object.keys(constraint[validatorType])[0];
+ if (!validator) {
+ return;
+ }
+
+ const value = constraint[validatorType][validator];
+ if (value === void 0) {
+ return;
+ }
+
+ let comparator = validator;
+ if (validator === 'value') {
+ comparator = validatorType;
+ }
+
+ const attributeText = getAttributeText(attributeId, attributes);
+ const constraintText =
+ attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
+
+ return constraintText;
+}
diff --git a/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js b/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js
new file mode 100644
index 000000000..602cc4158
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js
@@ -0,0 +1,32 @@
+/**
+ * Removes the first instance of an element from an array, if an equal value exists
+ *
+ * @param array
+ * @param input
+ *
+ * @returns {boolean} Whether or not the element was found and removed
+ */
+const removeFromArray = (array, input) => {
+ // If the array is empty, stop here
+ if (!array || !array.length) {
+ return false;
+ }
+
+ array.forEach((value, index) => {
+ // TODO: Double check whether or not this deep equality check is necessary
+ //if (_.isEqual(value, input)) {
+ if (value === input) {
+ indexToRemove = index;
+ return false;
+ }
+ });
+
+ if (indexToRemove === void 0) {
+ return false;
+ }
+
+ array.splice(indexToRemove, 1);
+ return true;
+};
+
+export { removeFromArray };
diff --git a/platform/core/src/services/HangingProtocolService/lib/sortByScore.js b/platform/core/src/services/HangingProtocolService/lib/sortByScore.js
new file mode 100644
index 000000000..902072591
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/lib/sortByScore.js
@@ -0,0 +1,8 @@
+// Sorts an array by score
+const sortByScore = arr => {
+ arr.sort((a, b) => {
+ return b.score - a.score;
+ });
+};
+
+export { sortByScore };
diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.js b/platform/core/src/services/HangingProtocolService/lib/validator.js
new file mode 100644
index 000000000..454143a03
--- /dev/null
+++ b/platform/core/src/services/HangingProtocolService/lib/validator.js
@@ -0,0 +1,39 @@
+import validate from 'validate.js';
+
+validate.validators.equals = function (value, options, key, attributes) {
+ if (options && value !== options.value) {
+ return key + 'must equal ' + options.value;
+ }
+};
+
+validate.validators.doesNotEqual = function (value, options, key) {
+ if (options && value === options.value) {
+ return key + 'cannot equal ' + options.value;
+ }
+};
+
+validate.validators.contains = function (value, options, key) {
+ if (options && value.indexOf && value.indexOf(options.value) === -1) {
+ return key + 'must contain ' + options.value;
+ }
+};
+
+validate.validators.doesNotContain = function (value, options, key) {
+ if (options && value.indexOf && value.indexOf(options.value) !== -1) {
+ return key + 'cannot contain ' + options.value;
+ }
+};
+
+validate.validators.startsWith = function (value, options, key) {
+ if (options && value.startsWith && !value.startsWith(options.value)) {
+ return key + 'must start with ' + options.value;
+ }
+};
+
+validate.validators.endsWith = function (value, options, key) {
+ if (options && value.endsWith && !value.endsWith(options.value)) {
+ return key + 'must end with ' + options.value;
+ }
+};
+
+export default validate;
diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js
index e5d77f7a1..c4abff9fc 100644
--- a/platform/core/src/services/MeasurementService/MeasurementService.js
+++ b/platform/core/src/services/MeasurementService/MeasurementService.js
@@ -54,6 +54,7 @@ const EVENTS = {
MEASUREMENT_UPDATED: 'event::measurement_updated',
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
MEASUREMENT_ADDED: 'event::measurement_added',
+ RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
@@ -314,11 +315,17 @@ class MeasurementService {
* Converted to/from annotation in the same way. E.g. import serialized data
* Of the same form as the measurement source.
* @param {MeasurementSource} source The measurement source instance.
- * @param {string} definition The source definition you want to add the measuremnet to.
+ * @param {string} definition The source definition you want to add the measurement to.
* @param {object} data The data you wish to add to the source.
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
*/
- addRawMeasurement(source, definition, data, toMeasurementSchema) {
+ addRawMeasurement(
+ source,
+ definition,
+ data,
+ toMeasurementSchema,
+ dataSource = {}
+ ) {
if (!this._isValidSource(source)) {
log.warn('Invalid source. Exiting early.');
return;
@@ -342,7 +349,6 @@ class MeasurementService {
try {
/* Convert measurement */
measurement = toMeasurementSchema(data);
-
/* Assign measurement source instance */
measurement.source = source;
} catch (error) {
@@ -385,9 +391,11 @@ class MeasurementService {
} else {
log.info(`Measurement added.`, newMeasurement);
this.measurements[internalId] = newMeasurement;
- this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
+ this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
source,
measurement: newMeasurement,
+ data,
+ dataSource,
});
}
@@ -501,6 +509,7 @@ class MeasurementService {
clearMeasurements() {
this.measurements = {};
+ this._jumpToMeasurementCache = {};
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED);
}
diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js
index fcb061443..ce88492e8 100644
--- a/platform/core/src/services/ToolBarService/ToolBarService.js
+++ b/platform/core/src/services/ToolBarService/ToolBarService.js
@@ -13,6 +13,7 @@ export default class ToolBarService {
this.EVENTS = EVENTS;
this.listeners = {};
this.buttons = {};
+ this.unsubscriptions = []; // if tools need to unsubscribe from events
this.buttonSections = {
/**
* primary: ['Zoom', 'Wwwc'],
@@ -39,6 +40,16 @@ export default class ToolBarService {
this.extensionManager = extensionManager;
}
+ reset() {
+ this.unsubscriptions.forEach(unsub => unsub());
+ this.state = {
+ primaryToolId: 'Wwwc',
+ toggles: {},
+ groups: {},
+ };
+ this.unsubscriptions = [];
+ }
+
/**
*
* @param {*} interaction
@@ -63,6 +74,7 @@ export default class ToolBarService {
this.state.toggles[itemId] === undefined
? true
: !this.state.toggles[itemId];
+ interaction.commandOptions.toggledState = this.state.toggles[itemId];
break;
}
default:
@@ -73,13 +85,21 @@ export default class ToolBarService {
//
// NOTE: Should probably just do this for tools as well?
// But would be nice if we could enforce at least the command name?
+ let unsubscribe;
if (interaction.commandName) {
- commandsManager.runCommand(
+ unsubscribe = commandsManager.runCommand(
interaction.commandName,
interaction.commandOptions
);
}
+ // Storing the unsubscribe for later reseting
+ if (unsubscribe && typeof unsubscribe === 'function') {
+ if (this.unsubscriptions.indexOf(unsubscribe) === -1) {
+ this.unsubscriptions.push(unsubscribe);
+ }
+ }
+
// Track last touched id for each group
if (groupId) {
this.state.groups[groupId] = itemId;
diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js
index 85eb748ac..2f60e2966 100644
--- a/platform/core/src/services/index.js
+++ b/platform/core/src/services/index.js
@@ -10,6 +10,7 @@ import ToolBarService from './ToolBarService';
import ViewportGridService from './ViewportGridService';
import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService';
+import pubSubServiceInterface from './_shared/pubSubServiceInterface';
export {
MeasurementService,
@@ -23,5 +24,6 @@ export {
ToolBarService,
ViewportGridService,
HangingProtocolService,
- CineService
+ CineService,
+ pubSubServiceInterface,
};
diff --git a/platform/core/src/utils/StackManager.js b/platform/core/src/utils/StackManager.js
index 241f0f764..942928292 100644
--- a/platform/core/src/utils/StackManager.js
+++ b/platform/core/src/utils/StackManager.js
@@ -22,6 +22,7 @@ function createAndAddStack(
StudyInstanceUID,
frameRate,
isClip,
+ initialImageIdIndex,
} = displaySet;
if (!images) {
return;
@@ -35,6 +36,7 @@ function createAndAddStack(
imageIds,
frameRate,
isClip,
+ initialImageIdIndex,
};
stackMap[displaySetInstanceUID] = stack;
diff --git a/platform/core/src/utils/sortBySeriesDate.js b/platform/core/src/utils/sortBySeriesDate.js
index 684a85e0e..1a694934c 100644
--- a/platform/core/src/utils/sortBySeriesDate.js
+++ b/platform/core/src/utils/sortBySeriesDate.js
@@ -2,15 +2,16 @@
* Sorting function
* Sorts an array by seriesDate and seriesNumber if equal
*/
-const sortBySeriesDate = array => array.sort((a, b) => {
- if (a.seriesNumber !== b.seriesNumber) {
- return a.seriesNumber - b.seriesNumber;
- }
+const sortBySeriesDate = array =>
+ array.sort((a, b) => {
+ if (a.seriesNumber !== b.seriesNumber) {
+ return a.seriesNumber - b.seriesNumber;
+ }
- const seriesDateA = Date.parse(a.seriesDate);
- const seriesDateB = Date.parse(b.seriesDate);
+ const seriesDateA = Date.parse(a.seriesDate);
+ const seriesDateB = Date.parse(b.seriesDate);
- return seriesDateA - seriesDateB;
-});
+ return seriesDateA - seriesDateB;
+ });
export default sortBySeriesDate;
diff --git a/platform/ui/index.js b/platform/ui/index.js
index bd1ace9d0..77e5a7396 100644
--- a/platform/ui/index.js
+++ b/platform/ui/index.js
@@ -25,9 +25,6 @@ export {
ViewportGridContext,
ViewportGridProvider,
useViewportGrid,
- HangingProtocolContext,
- HangingProtocolProvider,
- useHangingProtocol,
} from './src/contextProviders';
/** COMPONENTS */
diff --git a/platform/ui/src/components/Header/Header.jsx b/platform/ui/src/components/Header/Header.jsx
index dae607334..34cc31213 100644
--- a/platform/ui/src/components/Header/Header.jsx
+++ b/platform/ui/src/components/Header/Header.jsx
@@ -6,7 +6,7 @@ import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
-function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
+function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
const { t } = useTranslation();
const history = useHistory();
@@ -18,6 +18,10 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
}
};
+ const CustomLogo = (React) => {
+ return WhiteLabeling.createLogoComponentFn(React)
+ }
+
return (
@@ -29,7 +33,7 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
onClick={onReturnHandler}
>
{isReturnEnabled &&
}
-
+
{WhiteLabeling ? CustomLogo(React) : }