@@ -64,7 +69,7 @@ const ToolbarButton = ({
id={id}
{...rest}
>
-
+ {iconEl}
diff --git a/platform/ui/src/components/Viewport/Viewport.tsx b/platform/ui/src/components/Viewport/Viewport.tsx
index a9576c1c9..bcd2f5a2e 100644
--- a/platform/ui/src/components/Viewport/Viewport.tsx
+++ b/platform/ui/src/components/Viewport/Viewport.tsx
@@ -2,7 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import { LegacyViewportActionBar, Notification } from '../';
-const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
+const Viewport = ({
+ viewportId,
+ viewportIndex,
+ onArrowsClick,
+ studyData,
+ children,
+}) => {
+ if (!viewportId) {
+ viewportId = `viewport-${viewportIndex}`;
+ }
return (
@@ -39,7 +48,7 @@ const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
{/* STUDY IMAGE */}
-
diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx
index 80a1647d9..b11f1d301 100644
--- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx
+++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx
@@ -6,17 +6,22 @@ import React, {
useReducer,
} from 'react';
import PropTypes from 'prop-types';
-
+import isEqual from 'lodash.isequal';
import viewportLabels from '../utils/viewportLabels';
+import getPresentationId from './getPresentationId';
const DEFAULT_STATE = {
- numRows: null,
- numCols: null,
- layoutType: 'grid',
+ activeViewportIndex: 0,
+ layout: {
+ numRows: 0,
+ numCols: 0,
+ layoutType: 'grid',
+ },
viewports: [
{
displaySetInstanceUIDs: [],
viewportOptions: {},
+ displaySetSelectors: [],
displaySetOptions: [{}],
x: 0, // left
y: 0, // top
@@ -25,20 +30,58 @@ const DEFAULT_STATE = {
viewportLabel: null,
},
],
- activeViewportIndex: 0,
- cachedLayout: {},
};
export const ViewportGridContext = createContext(DEFAULT_STATE);
/**
- * Given the flatten index, and rows and column, it returns the
- * row and column index
+ * Find a viewport to re-use, and then set the viewportId
+ *
+ * @param idSet
+ * @param viewport
+ * @param stateViewports
+ * @returns
*/
-const unravelIndex = (index, numRows, numCols) => {
- const row = Math.floor(index / numCols);
- const col = index % numCols;
- return { row, col };
+const reuseViewport = (idSet, viewport, stateViewports) => {
+ const oldIds = {};
+ for (const oldViewport of stateViewports) {
+ const { viewportId: oldId } = oldViewport;
+ oldIds[oldId] = true;
+ if (!oldId || idSet[oldId]) continue;
+ if (
+ !isEqual(
+ oldViewport.displaySetInstanceUIDs,
+ viewport.displaySetInstanceUIDs
+ )
+ ) {
+ continue;
+ }
+ idSet[oldId] = true;
+ // TODO re-use viewports once the flickering/wrong size redraw is fixed
+ // return {
+ // ...oldViewport,
+ // ...viewport,
+ // viewportOptions: {
+ // ...oldViewport.viewportOptions,
+
+ // viewportId: oldViewport.viewportId,
+ // },
+ // };
+ }
+ // Find a viewport instance number different from earlier viewports having
+ // the same presentationId as this one would - will be less than 10k
+ // viewports hopefully :-)
+ for (let i = 0; i < 10000; i++) {
+ const viewportId = 'viewport-' + i;
+ if (idSet[viewportId] || oldIds[viewportId]) continue;
+ idSet[viewportId] = true;
+ return {
+ ...viewport,
+ viewportId,
+ viewportOptions: { ...viewport.viewportOptions, viewportId },
+ };
+ }
+ throw new Error('No ID found');
};
export function ViewportGridProvider({ children, service }) {
@@ -59,26 +102,40 @@ export function ViewportGridProvider({ children, service }) {
// which might have been a PDF Viewport. The viewport itself
// will deal with inheritance if required. Here is just a simple
// provider.
- const viewportOptions = payload.viewportOptions || {};
- const displaySetOptions = payload.displaySetOptions || [{}];
+ const viewport = state.viewports[viewportIndex] || {};
+ const viewportOptions = { ...payload.viewportOptions };
+
+ const displaySetOptions = payload.displaySetOptions || [];
+ if (displaySetOptions.length === 0) {
+ // Only copy index 0, as that is all that is currently supported by this
+ // method call.
+ displaySetOptions.push({ ...viewport.displaySetOptions?.[0] });
+ }
const viewports = state.viewports.slice();
- if (!viewportOptions.viewportId) {
- viewportOptions.viewportId = `viewport-${viewportIndex}`;
- }
-
- // merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
- // into the viewport object at the given index
- viewports[viewportIndex] = {
- ...viewports[viewportIndex],
+ let newView = {
+ ...viewport,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
viewportLabel: viewportLabels[viewportIndex],
};
+ viewportOptions.presentationId = getPresentationId(newView, viewports);
- return { ...state, ...{ viewports } };
+ // Make sure we assign a viewport id
+ newView = reuseViewport({}, newView, state.viewports);
+ console.log(
+ 'Creating new viewport',
+ viewportIndex,
+ newView.viewportOptions.viewportId,
+ displaySetInstanceUIDs,
+ displaySetOptions
+ );
+
+ viewports[viewportIndex] = newView;
+
+ return { ...state, viewports };
}
case 'SET_LAYOUT': {
const {
@@ -86,108 +143,96 @@ export function ViewportGridProvider({ children, service }) {
numRows,
layoutOptions,
layoutType = 'grid',
- keepExtraViewports = false,
+ findOrCreateViewport,
} = action.payload;
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
- const numPanes = layoutOptions.length || numRows * numCols;
- const viewports = state.viewports.slice();
- const activeViewportIndex =
- state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex;
+ const hasOptions = layoutOptions?.length;
+ const viewports = [];
- while (viewports.length < numPanes) {
- viewports.push({});
- }
+ // Options is a temporary state store which can be used by the
+ // findOrCreate to store state about already found viewports. Typically,
+ // it will be used to store the display set UID's which are already
+ // in view so that the find or create can decide which display sets
+ // haven't been viewed yet, and add them in the appropriate order.
+ const options = {};
- // Extra viewports are kept when the grid layout is changed in the UI
- // because the user populated those viewports and if the viewports were to
- // return on screen their contents should be maintained.
- if (!keepExtraViewports) {
- while (viewports.length > numPanes) {
- viewports.pop();
+ let activeViewportIndex;
+ for (let row = 0; row < numRows; row++) {
+ for (let col = 0; col < numCols; col++) {
+ const pos = col + row * numCols;
+ const layoutOption = layoutOptions[pos];
+ const positionId = layoutOption?.positionId || `${col}-${row}`;
+ if (hasOptions && pos >= layoutOptions.length) {
+ continue;
+ }
+ if (
+ !activeViewportIndex ||
+ state.viewports[pos]?.positionId === positionId
+ ) {
+ activeViewportIndex = pos;
+ }
+ const viewport = findOrCreateViewport(pos, positionId, options);
+ if (!viewport) continue;
+ viewport.positionId = positionId;
+ // Create a new viewport object as it is getting updated here
+ // and it is part of the read only state
+ viewports.push(viewport);
+ let xPos, yPos, w, h;
+
+ if (layoutOptions && layoutOptions[pos]) {
+ ({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[pos]);
+ } else {
+ w = 1 / numCols;
+ h = 1 / numRows;
+ xPos = col * w;
+ yPos = row * h;
+ }
+
+ viewport.width = w;
+ viewport.height = h;
+ viewport.x = xPos;
+ viewport.y = yPos;
}
}
- for (let i = 0; i < numPanes; i++) {
- let xPos, yPos, w, h;
-
- if (layoutOptions && layoutOptions[i]) {
- ({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[i]);
- } else {
- const { row, col } = unravelIndex(i, numRows, numCols);
- w = 1 / numCols;
- h = 1 / numRows;
- xPos = col * w;
- yPos = row * h;
+ const viewportIdSet = {};
+ for (
+ let viewportIndex = 0;
+ viewportIndex < viewports.length;
+ viewportIndex++
+ ) {
+ const viewport = reuseViewport(
+ viewportIdSet,
+ viewports[viewportIndex],
+ state.viewports
+ );
+ if (!viewport.viewportOptions.presentationId) {
+ viewport.viewportOptions.presentationId = getPresentationId(
+ viewport,
+ viewports
+ );
}
-
- viewports[i].width = w;
- viewports[i].height = h;
- viewports[i].x = xPos;
- viewports[i].y = yPos;
+ viewport.viewportIndex = viewportIndex;
+ viewport.viewportLabel = viewportLabels[viewportIndex];
+ viewports[viewportIndex] = viewport;
}
- return {
+ const ret = {
...state,
- ...{
- activeViewportIndex,
+ activeViewportIndex,
+ layout: {
+ ...state.layout,
numCols,
numRows,
layoutType,
- viewports,
},
+ viewports,
};
+ return ret;
}
case 'RESET': {
- return {
- numCols: null,
- numRows: null,
- layoutType: 'grid',
- activeViewportIndex: 0,
- viewports: [
- {
- displaySetInstanceUIDs: [],
- displaySetOptions: [],
- viewportOptions: {},
- x: 0, // left
- y: 0, // top
- width: 100,
- height: 100,
- },
- ],
- cachedLayout: {},
- };
- }
-
- // The SET_CACHE_LAYOUT action can be used for caching a layout
- // for instance double clicking a viewport to maximize it.
- // and then restoring the previous layout when the viewport is
- // double clicked again.
- case 'SET_CACHED_LAYOUT': {
- const { cacheId, cachedLayout } = action.payload;
-
- // deep copy the cachedLayout into the state
- return {
- ...state,
- cachedLayout: {
- ...state.cachedLayout,
- [cacheId]: JSON.parse(JSON.stringify(cachedLayout)),
- },
- };
- }
-
- case 'RESTORE_CACHED_LAYOUT': {
- const cacheId = action.payload;
-
- if (!state.cachedLayout[cacheId]) {
- console.warn(
- `No cached layout found for cacheId: ${cacheId}. Ignoring...`
- );
- return state;
- }
-
- const cachedLayout = state.cachedLayout;
- return { ...state.cachedLayout[cacheId], cachedLayout };
+ return DEFAULT_STATE;
}
case 'SET': {
@@ -221,6 +266,7 @@ export function ViewportGridProvider({ children, service }) {
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
+ displaySetSelectors,
displaySetOptions,
}) =>
dispatch({
@@ -229,6 +275,7 @@ export function ViewportGridProvider({ children, service }) {
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
+ displaySetSelectors,
displaySetOptions,
},
}),
@@ -250,7 +297,7 @@ export function ViewportGridProvider({ children, service }) {
numRows,
numCols,
layoutOptions = [],
- keepExtraViewports = false,
+ findOrCreateViewport,
}) =>
dispatch({
type: 'SET_LAYOUT',
@@ -259,7 +306,7 @@ export function ViewportGridProvider({ children, service }) {
numRows,
numCols,
layoutOptions,
- keepExtraViewports,
+ findOrCreateViewport,
},
}),
[dispatch]
@@ -274,25 +321,6 @@ export function ViewportGridProvider({ children, service }) {
[dispatch]
);
- const setCachedLayout = useCallback(
- payload =>
- dispatch({
- type: 'SET_CACHED_LAYOUT',
- payload,
- }),
- [dispatch]
- );
-
- const restoreCachedLayout = useCallback(
- cacheId => {
- dispatch({
- type: 'RESTORE_CACHED_LAYOUT',
- payload: cacheId,
- });
- },
- [dispatch]
- );
-
const set = useCallback(
payload =>
dispatch({
@@ -303,7 +331,8 @@ export function ViewportGridProvider({ children, service }) {
);
const getNumViewportPanes = useCallback(() => {
- const { numCols, numRows, viewports } = viewportGridState;
+ const { layout, viewports } = viewportGridState;
+ const { numRows, numCols } = layout;
return Math.min(viewports.length, numCols * numRows);
}, [viewportGridState]);
@@ -322,8 +351,6 @@ export function ViewportGridProvider({ children, service }) {
setLayout,
reset,
onModeExit: reset,
- setCachedLayout,
- restoreCachedLayout,
set,
getNumViewportPanes,
});
@@ -336,8 +363,6 @@ export function ViewportGridProvider({ children, service }) {
setDisplaySetsForViewports,
setLayout,
reset,
- setCachedLayout,
- restoreCachedLayout,
set,
getNumViewportPanes,
]);
@@ -348,8 +373,6 @@ export function ViewportGridProvider({ children, service }) {
setDisplaySetsForViewport,
setDisplaySetsForViewports,
setLayout,
- setCachedLayout,
- restoreCachedLayout,
reset,
set,
getNumViewportPanes,
diff --git a/platform/ui/src/contextProviders/getPresentationId.ts b/platform/ui/src/contextProviders/getPresentationId.ts
new file mode 100644
index 000000000..7b41eaf8a
--- /dev/null
+++ b/platform/ui/src/contextProviders/getPresentationId.ts
@@ -0,0 +1,73 @@
+/**
+ * Selects a presentation ID to use for this viewport.
+ * This is done to allow the same display set to be displayed more than once
+ * on screen, with different attributes such as window level and initial position.
+ * Then, when redisplaying that, the nearest/most common attribute is re-used.
+ *
+ * For example, for display set
, in a viewport of type volume,
+ * the generated presentationID might be
+ * `volume:axial:`. This can then be used to store and retrieve
+ * presentation information in state sync service 'presentationSync' state.
+ *
+ * The generated value attempts to generate a unique value for every type
+ * of viewport which should have it's own presentation information. Thus, the
+ * following values are used for presentation ID:
+ *
+ * 1. viewportType - since the presentation information for a volume is different than for a stack
+ * 2. orientation - since the camera is different for different orientations
+ * 3. display set instance UID - since different display sets should get displayed differently
+ * 4. instance count - since displaying the same series twice should allow applying different window level etc
+ *
+ * @param viewport requiring a presentation Id
+ * @param viewports is the list of viewports being shown. Any presentation ID's
+ * among them must not be re-used in order to have each viewport have it's own presentation ID.
+ * @returns Presentation ID id, or undefined if nothing displayed
+ */
+const getPresentationId = (viewport, viewports): string => {
+ if (!viewport) return;
+ const { viewportOptions, displaySetInstanceUIDs } = viewport;
+ if (!viewportOptions || !displaySetInstanceUIDs?.length) {
+ console.log('No viewport type or display sets in', viewport);
+ return;
+ }
+
+ const viewportType = viewportOptions.viewportType || 'stack';
+ const idArr = [viewportType, 0, ...displaySetInstanceUIDs];
+ if (viewportOptions.orientation) {
+ idArr.splice(2, 0, viewportOptions.orientation);
+ }
+
+ // Allow setting a custom presentation prefix in the hanging protocol
+ // This allows defining new
+ // presentation groups to be set automatically when one knows that the
+ // same display set will be displayed in different ways.
+ // This is the recommended way to manage a hanging protocol which displays
+ // multiple views of a single display set, eg to display brain, bone, soft
+ // tissue views in different viewports.
+ if (viewportOptions.presentationPrefix) {
+ idArr.push(viewportOptions.presentationPrefix);
+ }
+ if (!viewports) {
+ console.log('viewports not defined', idArr.join(','));
+ return idArr.join('&');
+ }
+
+ // This code finds the first unique index to add to the presentation id so that
+ // two viewports containing the same display set in the same type of viewport
+ // can have different presentation information. This allows comparison of
+ // a single display set in two or more viewports, when the user has simply
+ // dragged and dropped the view in twice. For example, it allows displaying
+ // bone, brain and soft tissue views of a single display set, and to still
+ // remember the specific changes to each viewport.
+ for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
+ idArr[1] = displayInstance;
+ const testId = idArr.join('&');
+ if (!viewports.find(it => it.viewportOptions?.presentationId === testId)) {
+ break;
+ }
+ }
+ const id = idArr.join('&');
+ return id;
+};
+
+export default getPresentationId;
diff --git a/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js b/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js
new file mode 100644
index 000000000..c7b8a8417
--- /dev/null
+++ b/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js
@@ -0,0 +1,31 @@
+describe('OHIF HP', () => {
+ beforeEach(() => {
+ cy.checkStudyRouteInViewer(
+ '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
+ '&hangingProtocolId=@ohif/hp-extension.mn'
+ );
+ cy.expectMinimumThumbnails(3);
+ cy.initCornerstoneToolsAliases();
+ cy.initCommonElementsAliases();
+ });
+
+ it('Should display 3 up', () => {
+ cy.get('[data-cy="viewport-pane"]')
+ .its('length')
+ .should('be.eq', 3);
+ });
+
+ it('Should navigate next/previous stage', () => {
+ cy.get('body').type(',');
+ cy.wait(250);
+ cy.get('[data-cy="viewport-pane"]')
+ .its('length')
+ .should('be.eq', 4);
+
+ cy.get('body').type('..');
+ cy.wait(250);
+ cy.get('[data-cy="viewport-pane"]')
+ .its('length')
+ .should('be.eq', 2);
+ });
+});
diff --git a/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js b/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js
index 764413acc..8264c9635 100644
--- a/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js
+++ b/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js
@@ -17,8 +17,21 @@ describe('OHIF Study Viewer Page', function() {
});
it('drags and drop a series thumbnail into viewport', function() {
- cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
- .drag('.cornerstone-canvas'); //dropzone element
+ // Can't use the native drag version as the element should be rerendered
+ // cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
+ // .drag('.cornerstone-canvas'); //dropzone element
+
+ const dataTransfer = new DataTransfer();
+
+ cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)')
+ .first()
+ .trigger('mousedown', { which: 1, button: 0 })
+ .trigger('dragstart', { dataTransfer })
+ .trigger('drag', {});
+ cy.get('.cornerstone-canvas')
+ .trigger('mousemove', 'center')
+ .trigger('dragover', { dataTransfer, force: true })
+ .trigger('drop', { dataTransfer, force: true });
//const expectedText =
// 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
diff --git a/platform/viewer/cypress/support/commands.js b/platform/viewer/cypress/support/commands.js
index eadd001bb..df760db10 100644
--- a/platform/viewer/cypress/support/commands.js
+++ b/platform/viewer/cypress/support/commands.js
@@ -54,20 +54,29 @@ Cypress.Commands.add('openStudy', PatientName => {
.click({ force: true });
});
-Cypress.Commands.add('checkStudyRouteInViewer', StudyInstanceUID => {
- cy.location('pathname').then($url => {
- cy.log($url);
- if ($url == 'blank' || !$url.includes(`/basic-test/${StudyInstanceUID}`)) {
- cy.openStudyInViewer(StudyInstanceUID);
- cy.waitDicomImage();
- cy.wait(2000);
- }
- });
-});
+Cypress.Commands.add(
+ 'checkStudyRouteInViewer',
+ (StudyInstanceUID, otherParams = '') => {
+ cy.location('pathname').then($url => {
+ cy.log($url);
+ if (
+ $url == 'blank' ||
+ !$url.includes(`/basic-test/${StudyInstanceUID}${otherParams}`)
+ ) {
+ cy.openStudyInViewer(StudyInstanceUID, otherParams);
+ cy.waitDicomImage();
+ cy.wait(2000);
+ }
+ });
+ }
+);
-Cypress.Commands.add('openStudyInViewer', StudyInstanceUID => {
- cy.visit(`/basic-test?StudyInstanceUIDs=${StudyInstanceUID}`);
-});
+Cypress.Commands.add(
+ 'openStudyInViewer',
+ (StudyInstanceUID, otherParams = '') => {
+ cy.visit(`/basic-test?StudyInstanceUIDs=${StudyInstanceUID}${otherParams}`);
+ }
+);
/**
* Command to search for a Modality and open the study.
diff --git a/platform/viewer/public/config/multiple.js b/platform/viewer/public/config/multiple.js
index ca5c9d848..7179bb74b 100644
--- a/platform/viewer/public/config/multiple.js
+++ b/platform/viewer/public/config/multiple.js
@@ -1,4 +1,7 @@
window.config = {
+ // Activate the new HP mode....
+ isNewHP: true,
+
routerBasename: '/',
customizationService: [
'@ohif/extension-default.customizationModule.datasources',
@@ -59,25 +62,6 @@ window.config = {
singlepart: 'bulkdata,video,pdf',
},
},
- {
- friendlyName: 'dcmjs DICOMWeb Server',
- namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
- sourceName: 'shared',
- configuration: {
- name: 'shared',
- qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
- wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
- qidoSupportsIncludeField: false,
- supportsReject: false,
- imageRendering: 'wadors',
- thumbnailRendering: 'wadors',
- enableStudyLazyLoad: true,
- supportsFuzzyMatching: false,
- supportsWildcard: true,
- staticWado: true,
- singlepart: 'bulkdata,video,pdf',
- },
- },
{
friendlyName: 'E2E Test Data',
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
@@ -122,92 +106,7 @@ window.config = {
console.warn('test, navigate to https://ohif.org/');
},
defaultDataSourceName: 'default',
- hotkeys: [
- {
- commandName: 'incrementActiveViewport',
- label: 'Next Viewport',
- keys: ['right'],
- },
- {
- commandName: 'decrementActiveViewport',
- label: 'Previous Viewport',
- keys: ['left'],
- },
- { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
- { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
- { commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
- {
- commandName: 'flipViewportHorizontal',
- label: 'Flip Horizontally',
- keys: ['h'],
- },
- {
- commandName: 'flipViewportVertical',
- label: 'Flip Vertically',
- keys: ['v'],
- },
- { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
- { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
- { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
- { commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
- { commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
- { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
- // {
- // commandName: 'previousViewportDisplaySet',
- // label: 'Previous Series',
- // keys: ['pagedown'],
- // },
- // {
- // commandName: 'nextViewportDisplaySet',
- // label: 'Next Series',
- // keys: ['pageup'],
- // },
- { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
- // ~ Window level presets
- {
- commandName: 'windowLevelPreset1',
- label: 'W/L Preset 1',
- keys: ['1'],
- },
- {
- commandName: 'windowLevelPreset2',
- label: 'W/L Preset 2',
- keys: ['2'],
- },
- {
- commandName: 'windowLevelPreset3',
- label: 'W/L Preset 3',
- keys: ['3'],
- },
- {
- commandName: 'windowLevelPreset4',
- label: 'W/L Preset 4',
- keys: ['4'],
- },
- {
- commandName: 'windowLevelPreset5',
- label: 'W/L Preset 5',
- keys: ['5'],
- },
- {
- commandName: 'windowLevelPreset6',
- label: 'W/L Preset 6',
- keys: ['6'],
- },
- {
- commandName: 'windowLevelPreset7',
- label: 'W/L Preset 7',
- keys: ['7'],
- },
- {
- commandName: 'windowLevelPreset8',
- label: 'W/L Preset 8',
- keys: ['8'],
- },
- {
- commandName: 'windowLevelPreset9',
- label: 'W/L Preset 9',
- keys: ['9'],
- },
- ],
+
+ // Only list the unique hotkeys
+ hotkeys: [],
};
diff --git a/platform/viewer/src/App.tsx b/platform/viewer/src/App.tsx
index f0f6d669d..44c3fd754 100644
--- a/platform/viewer/src/App.tsx
+++ b/platform/viewer/src/App.tsx
@@ -5,7 +5,12 @@ import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import Compose from './routes/Mode/Compose';
-
+import {
+ ServicesManager,
+ ExtensionManager,
+ CommandsManager,
+ HotkeysManager,
+} from '@ohif/core';
import {
DialogProvider,
Modal,
@@ -24,7 +29,10 @@ import createRoutes from './routes';
import appInit from './appInit.js';
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
-let commandsManager, extensionManager, servicesManager, hotkeysManager;
+let commandsManager: CommandsManager,
+ extensionManager: ExtensionManager,
+ servicesManager: ServicesManager,
+ hotkeysManager: HotkeysManager;
function App({ config, defaultExtensions, defaultModes }) {
const [init, setInit] = useState(null);
@@ -59,12 +67,12 @@ function App({ config, defaultExtensions, defaultModes }) {
} = appConfigState;
const {
- UIDialogService,
+ uiDialogService,
uiModalService,
- UINotificationService,
- UIViewportDialogService,
- ViewportGridService,
- CineService,
+ uiNotificationService,
+ uiViewportDialogService,
+ viewportGridService,
+ cineService,
userAuthenticationService,
customizationService,
} = servicesManager.services;
@@ -74,11 +82,11 @@ function App({ config, defaultExtensions, defaultModes }) {
[UserAuthenticationProvider, { service: userAuthenticationService }],
[I18nextProvider, { i18n }],
[ThemeWrapper],
- [ViewportGridProvider, { service: ViewportGridService }],
- [ViewportDialogProvider, { service: UIViewportDialogService }],
- [CineProvider, { service: CineService }],
- [SnackbarProvider, { service: UINotificationService }],
- [DialogProvider, { service: UIDialogService }],
+ [ViewportGridProvider, { service: viewportGridService }],
+ [ViewportDialogProvider, { service: uiViewportDialogService }],
+ [CineProvider, { service: cineService }],
+ [SnackbarProvider, { service: uiNotificationService }],
+ [DialogProvider, { service: uiDialogService }],
[ModalProvider, { service: uiModalService, modal: Modal }],
];
const CombinedProviders = ({ children }) =>
diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js
index dbe623b6c..3fd86e1ce 100644
--- a/platform/viewer/src/appInit.js
+++ b/platform/viewer/src/appInit.js
@@ -8,6 +8,7 @@ import {
UIDialogService,
UIViewportDialogService,
MeasurementService,
+ StateSyncService,
DisplaySetService,
ToolbarService,
ViewportGridService,
@@ -60,6 +61,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
CineService.REGISTRATION,
UserAuthenticationService.REGISTRATION,
PanelService.REGISTRATION,
+ StateSyncService.REGISTRATION,
]);
errorHandler.getHTTPErrorHandler = () => {
diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx
index 0c3d09fb9..70a9ed018 100644
--- a/platform/viewer/src/components/ViewportGrid.tsx
+++ b/platform/viewer/src/components/ViewportGrid.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
+import { ServicesManager } from '@ohif/core';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
import { utils } from '@ohif/core';
import EmptyViewport from './EmptyViewport';
@@ -22,11 +23,28 @@ const ORIENTATION_MAP = {
},
};
+const createHpInfo = (protocol, stage, activeStudyUID) => {
+ return {
+ hangingProtocolId: protocol.id,
+ stageId: stage.stageId,
+ stageIdx: protocol.stages.findIndex(it => it === stage),
+ activeStudyUID,
+ };
+};
+
+const compareViewportOptions = (opts1, opts2) => {
+ if ((opts1.viewportType || 'stack') != opts2.viewportType) {
+ return false;
+ }
+ return true;
+};
+
function ViewerViewportGrid(props) {
const { servicesManager, viewportComponents, dataSource } = props;
const [viewportGrid, viewportGridService] = useViewportGrid();
- const { numCols, numRows, activeViewportIndex, viewports } = viewportGrid;
+ const { layout, activeViewportIndex, viewports } = viewportGrid;
+ const { numCols, numRows } = layout;
// TODO -> Need some way of selecting which displaySets hit the viewports.
const {
@@ -34,124 +52,77 @@ function ViewerViewportGrid(props) {
measurementService,
hangingProtocolService,
uiNotificationService,
- } = servicesManager.services;
+ } = (servicesManager as ServicesManager).services;
/**
- * This callback runs only after displaySets have changed (created and added or modified)
+ * This callback runs after the viewports structure has changed in any way.
+ * On initial display, that means if it has changed by applying a HangingProtocol,
+ * while subsequently it may mean by changing the stage or by manually adjusting
+ * the layout.
+
*/
- const updateDisplaySetsForViewports = useCallback(
- availableDisplaySets => {
- if (!availableDisplaySets.length) {
+ const updateDisplaySetsFromProtocol = (
+ protocol,
+ stage,
+ activeStudyUID,
+ viewportMatchDetails
+ ) => {
+ const availableDisplaySets = displaySetService.getActiveDisplaySets();
+
+ if (!availableDisplaySets.length) {
+ console.log('No available display sets', availableDisplaySets);
+ return;
+ }
+
+ // Match each viewport individually
+ const { layoutType } = stage.viewportStructure;
+ const stageProps = stage.viewportStructure.properties;
+ const { columns: numCols, rows: numRows, layoutOptions = [] } = stageProps;
+
+ /**
+ * This find or create viewport uses the hanging protocol results to
+ * specify the viewport match details, which specifies the size and
+ * setup of the various viewports.
+ */
+ const findOrCreateViewport = viewportIndex => {
+ const details = viewportMatchDetails.get(viewportIndex);
+ if (!details) {
+ console.log('No match details for viewport', viewportIndex);
return;
}
- const {
- viewportMatchDetails,
- hpAlreadyApplied,
- } = hangingProtocolService.getMatchDetails();
-
- if (!viewportMatchDetails.size) {
- return;
- }
-
- const gridDisplaySetUIDs = [];
- const blankViewportIndices = [];
-
- // Match each viewport individually.
- const numViewports = viewportGridService.getNumViewportPanes();
-
- for (
- let viewportIndex = 0;
- viewportIndex < numViewports;
- viewportIndex++
- ) {
- const viewportDisplaySetUIDs =
- viewports[viewportIndex]?.displaySetInstanceUIDs ?? [];
-
- if (hpAlreadyApplied.get(viewportIndex)) {
- gridDisplaySetUIDs.push(...viewportDisplaySetUIDs);
- continue;
- }
-
- // if current viewport doesn't have a match
- if (viewportMatchDetails.get(viewportIndex) === undefined) {
- // if the current viewport is empty/blank
- if (viewportDisplaySetUIDs.length === 0) {
- blankViewportIndices.push(viewportIndex);
- } else {
- gridDisplaySetUIDs.push(...viewportDisplaySetUIDs);
- }
-
- continue;
- }
-
- const { displaySetsInfo, viewportOptions } = viewportMatchDetails.get(
- viewportIndex
- );
-
- const displaySetUIDsToHang = [];
- const displaySetUIDsToHangOptions = [];
- displaySetsInfo.forEach(
- ({ displaySetInstanceUID, displaySetOptions }) => {
- if (!displaySetInstanceUID) {
- return;
- }
+ const { displaySetsInfo, viewportOptions } = details;
+ const displaySetUIDsToHang = [];
+ const displaySetUIDsToHangOptions = [];
+ displaySetsInfo.forEach(
+ ({ displaySetInstanceUID, displaySetOptions }) => {
+ if (displaySetInstanceUID) {
displaySetUIDsToHang.push(displaySetInstanceUID);
- displaySetUIDsToHangOptions.push(displaySetOptions);
}
- );
- gridDisplaySetUIDs.push(...displaySetUIDsToHang);
-
- viewportGridService.setDisplaySetsForViewport({
- viewportIndex: viewportIndex,
- displaySetInstanceUIDs: displaySetUIDsToHang,
- viewportOptions,
- displaySetOptions: displaySetUIDsToHangOptions,
- });
-
- // During setting displaySets for viewport, we need to update the hanging protocol
- // but some viewports contain more than one display set (fusion), and their displaySet
- // will not be available at the time of setting displaySets for viewport. So we need to
- // update the hanging protocol after making sure all the matched display sets are available
- // and set on the viewport
- if (displaySetUIDsToHang.length === displaySetsInfo.length) {
- // The following will set the viewportsDisplaySetsMatched state
-
- const suppressEvent = true;
- const applied = true;
- hangingProtocolService.setHangingProtocolAppliedForViewport(
- viewportIndex,
- applied,
- suppressEvent
- );
+ displaySetUIDsToHangOptions.push(displaySetOptions);
}
- }
+ );
- blankViewportIndices.forEach((blankVPIndex: number) => {
- // try to fill the empty viewport with a display set not already in the grid
- const displaySetsNotInGrid = availableDisplaySets.filter(
- displaySet =>
- gridDisplaySetUIDs.indexOf(displaySet.displaySetInstanceUID) ===
- -1 &&
- ['SEG', 'SR', 'RTSTRUCT'].indexOf(displaySet.Modality) === -1
- );
+ return {
+ displaySetInstanceUIDs: displaySetUIDsToHang,
+ displaySetOptions: displaySetUIDsToHangOptions,
+ viewportOptions: {
+ ...viewportOptions,
+ },
+ };
+ };
- if (displaySetsNotInGrid.length > 0) {
- const displaySetUIDToAdd =
- displaySetsNotInGrid[0].displaySetInstanceUID;
- gridDisplaySetUIDs.push(displaySetUIDToAdd);
-
- viewportGridService.setDisplaySetsForViewport({
- viewportIndex: blankVPIndex,
- displaySetInstanceUIDs: [displaySetUIDToAdd],
- });
- }
- });
- },
- [viewportGrid, numRows, numCols]
- );
+ viewportGridService.setLayout({
+ numRows,
+ numCols,
+ layoutType,
+ layoutOptions,
+ hpInfo: createHpInfo(protocol, stage, activeStudyUID),
+ findOrCreateViewport,
+ });
+ };
const _getUpdatedViewports = useCallback(
(viewportIndex, displaySetInstanceUID) => {
@@ -177,22 +148,17 @@ function ViewerViewportGrid(props) {
[hangingProtocolService, uiNotificationService]
);
- useEffect(() => {
- const displaySets = displaySetService.getActiveDisplaySets();
- updateDisplaySetsForViewports(displaySets);
- }, [numRows, numCols]);
-
- // Layout change based on hanging protocols
+ // Using Hanging protocol engine to match the displaySets
useEffect(() => {
const { unsubscribe } = hangingProtocolService.subscribe(
- hangingProtocolService.EVENTS.NEW_LAYOUT,
- ({ layoutType, numRows, numCols, layoutOptions }) => {
- viewportGridService.setLayout({
- numRows,
- numCols,
- layoutType,
- layoutOptions,
- });
+ hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
+ ({ protocol, stage, activeStudyUID, viewportMatchDetails }) => {
+ updateDisplaySetsFromProtocol(
+ protocol,
+ stage,
+ activeStudyUID,
+ viewportMatchDetails
+ );
}
);
@@ -201,35 +167,6 @@ function ViewerViewportGrid(props) {
};
}, []);
- // Using Hanging protocol engine to match the displaySets
- useEffect(() => {
- const { unsubscribe } = hangingProtocolService.subscribe(
- hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
- () => {
- const displaySets = displaySetService.getActiveDisplaySets();
- updateDisplaySetsForViewports(displaySets);
- }
- );
-
- return () => {
- unsubscribe();
- };
- }, [viewports]);
-
- useEffect(() => {
- const { unsubscribe } = hangingProtocolService.subscribe(
- hangingProtocolService.EVENTS.STAGE_CHANGE,
- () => {
- const displaySets = DisplaySetService.getActiveDisplaySets();
- updateDisplaySetsForViewports(displaySets);
- }
- );
-
- return () => {
- unsubscribe();
- };
- }, [viewports]);
-
useEffect(() => {
const { unsubscribe } = measurementService.subscribe(
measurementService.EVENTS.JUMP_TO_MEASUREMENT,
@@ -366,11 +303,15 @@ function ViewerViewportGrid(props) {
const getViewportPanes = useCallback(() => {
const viewportPanes = [];
- const numViewports = viewportGridService.getNumViewportPanes();
- for (let i = 0; i < numViewports; i++) {
+ const numViewportPanes = viewportGridService.getNumViewportPanes();
+ for (let i = 0; i < numViewportPanes; i++) {
const viewportIndex = i;
const isActive = activeViewportIndex === viewportIndex;
const paneMetadata = viewports[i] || {};
+ const viewportId = paneMetadata.viewportId || `viewport-${i}`;
+ if (!paneMetadata.viewportId) {
+ paneMetadata.viewportId = viewportId;
+ }
const {
displaySetInstanceUIDs,
viewportOptions,
@@ -420,7 +361,7 @@ function ViewerViewportGrid(props) {
viewportPanes[i] = (
1 ? viewportLabel : ''}
+ viewportLabel={viewports.length > 1 ? viewportLabel : ''}
dataSource={dataSource}
viewportOptions={viewportOptions}
displaySetOptions={displaySetOptions}
@@ -456,7 +398,7 @@ function ViewerViewportGrid(props) {
}, [viewports, activeViewportIndex, viewportComponents, dataSource]);
/**
- * Loading indicator until numCols and numRows are gotten from the hangingProtocolService
+ * Loading indicator until numCols and numRows are gotten from the HangingProtocolService
*/
if (!numRows || !numCols) {
return null;
@@ -472,6 +414,7 @@ function ViewerViewportGrid(props) {
ViewerViewportGrid.propTypes = {
viewportComponents: PropTypes.array.isRequired,
+ servicesManager: PropTypes.instanceOf(ServicesManager),
};
ViewerViewportGrid.defaultProps = {
diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx
index ad110902c..0eb3f5a0a 100644
--- a/platform/viewer/src/routes/Mode/Mode.tsx
+++ b/platform/viewer/src/routes/Mode/Mode.tsx
@@ -8,6 +8,7 @@ import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import { useQuery, useSearchParams } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
+import getStudies from './studiesList';
/**
* Initialize the route.
@@ -64,21 +65,9 @@ function defaultRouteInit(
return;
}
- const studyMap = {};
+ // Gets the studies list to use
+ const studies = getStudies(studyInstanceUIDs, displaySets);
- // Prior studies don't quite work properly yet, but the studies list
- // is at least being generated and passed in.
- const studies = displaySets.reduce((prev, curr) => {
- const { StudyInstanceUID } = curr;
- if (!studyMap[StudyInstanceUID]) {
- const study = DicomMetadataStore.getStudy(StudyInstanceUID);
- studyMap[StudyInstanceUID] = study;
- prev.push(study);
- }
- return prev;
- }, []);
-
- // The assumption is that the display set at position 0 is the first
// study being displayed, and is thus the "active" study.
const activeStudy = studies[0];
@@ -133,10 +122,8 @@ export default function ModeRoute({
extensionManager.setActiveDataSource(dataSourceName);
- const dataSources = extensionManager.getActiveDataSource();
+ const dataSource = extensionManager.getActiveDataSource()[0];
- // Only handling one instance of the datasource type (E.g. one DICOMWeb server)
- const dataSource = dataSources[0];
// Only handling one route per mode for now
const route = mode.routes[0];
diff --git a/platform/viewer/src/routes/Mode/studiesList.ts b/platform/viewer/src/routes/Mode/studiesList.ts
new file mode 100644
index 000000000..ff45f8cc0
--- /dev/null
+++ b/platform/viewer/src/routes/Mode/studiesList.ts
@@ -0,0 +1,65 @@
+import { DicomMetadataStore, Types } from '@ohif/core';
+
+type StudyMetadata = Types.StudyMetadata;
+
+/**
+ * Compare function for sorting
+ *
+ * @param a - some simple value (string, number, timestamp)
+ * @param b - some simple value
+ * @param defaultCompare - default return value as a fallback when a===b
+ * @returns - compare a and b, returning 1 if ab and defaultCompare otherwise
+ */
+const compare = (a, b, defaultCompare = 0): number => {
+ if (a === b) return defaultCompare;
+ if (a < b) return 1;
+ return -1;
+};
+
+/**
+ * The studies from display sets gets the studies in study date
+ * order or in study instance UID order - not very useful, but
+ * if not specifically specified then at least making it consistent is useful.
+ */
+const getStudiesfromDisplaySets = (displaysets): StudyMetadata[] => {
+ const studyMap = {};
+
+ const ret = displaySets.reduce((prev, curr) => {
+ const { StudyInstanceUID } = curr;
+ if (!studyMap[StudyInstanceUID]) {
+ const study = DicomMetadataStore.getStudy(StudyInstanceUID);
+ studyMap[StudyInstanceUID] = study;
+ prev.push(study);
+ }
+ return prev;
+ }, []);
+ // Return the sorted studies, first on study date and second on study instance UID
+ ret.sort((a, b) => {
+ return compare(
+ a.StudyDate,
+ b.StudyDate,
+ compare(a.StudyInstanceUID, b.StudyInstanceUID)
+ );
+ });
+ return ret;
+};
+
+/**
+ * The studies retrieve from the Uids is faster and gets the studies
+ * in the original order, as specified.
+ */
+const getStudiesFromUIDs = (studyUids: string[]): StudyMetadata[] => {
+ if (!studyUids?.length) return;
+ return studyUids.map(uid => DicomMetadataStore.getStudy(uid));
+};
+
+/** Gets the array of studies */
+const getStudies = (studyUids?: string[], displaySets): StudyMetadata[] => {
+ return (
+ getStudiesFromUIDs(studyUids) || getStudiesfromDisplaySets(displaySets)
+ );
+};
+
+export default getStudies;
+
+export { getStudies, getStudiesFromUIDs, getStudiesfromDisplaySets, compare };
diff --git a/platform/viewer/src/routes/WorkList/WorkList.tsx b/platform/viewer/src/routes/WorkList/WorkList.tsx
index 2a6912d4b..e2e1fde2c 100644
--- a/platform/viewer/src/routes/WorkList/WorkList.tsx
+++ b/platform/viewer/src/routes/WorkList/WorkList.tsx
@@ -501,11 +501,9 @@ const defaultFilterValues = {
function _tryParseInt(str, defaultValue) {
let retValue = defaultValue;
- if (str != null) {
- if (str.length > 0) {
- if (!isNaN(str)) {
- retValue = parseInt(str);
- }
+ if (str && str.length > 0) {
+ if (!isNaN(str)) {
+ retValue = parseInt(str);
}
}
return retValue;