feat(grid): remove viewportIndex and only rely on viewportId (#3591)

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
AlirezaandBill Wallace authored and GitHub committed 2023-08-30 16:46:55 -04:00
1 parent 5dafac7c92
commit 4c6ff873e8
83 files changed
+1309 -1123

No files matched your search

@@ -5,121 +5,130 @@ import React, {
useEffect,
useReducer,
} from 'react';
import merge from 'lodash.merge';
import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal';
import { ViewportGridService } from '@ohif/core';
import { ViewportGridService, utils } from '@ohif/core';
import viewportLabels from '../utils/viewportLabels';
const DEFAULT_STATE = {
activeViewportIndex: 0,
interface Viewport {
viewportId: string;
displaySetInstanceUIDs: string[];
viewportOptions: any;
displaySetSelectors: any[];
displaySetOptions: any[];
x: number;
y: number;
width: number;
height: number;
viewportLabel: any;
}
interface Layout {
numRows: number;
numCols: number;
layoutType: string;
}
interface DefaultState {
activeViewportId: string | null;
layout: Layout;
viewports: Map<string, Viewport>;
}
const DEFAULT_STATE: DefaultState = {
activeViewportId: null,
layout: {
numRows: 0,
numCols: 0,
layoutType: 'grid',
},
viewports: [
{
displaySetInstanceUIDs: [],
viewportOptions: {},
displaySetSelectors: [],
displaySetOptions: [{}],
x: 0, // left
y: 0, // top
width: 100,
height: 100,
viewportLabel: null,
},
],
// Viewports structure has been changed to Map (previously it was
// tied to the viewportIndex which caused multiple issues. Now we have
// moved completely to viewportId which is unique for each viewport.
viewports: new Map(
Object.entries({
default: {
viewportId: 'default',
displaySetInstanceUIDs: [],
viewportOptions: {
viewportId: 'default',
},
displaySetSelectors: [],
displaySetOptions: [{}],
x: 0, // left
y: 0, // top
width: 100,
height: 100,
viewportLabel: null,
},
})
),
};
const getViewportLabel = (viewports, viewportId) => {
const viewportIds = Array.from(viewports.keys());
return viewportLabels[viewportIds.indexOf(viewportId)];
};
const determineActiveViewportId = (state: DefaultState, newViewports: Map) => {
const { activeViewportId } = state;
const currentActiveViewport = state.viewports.get(activeViewportId);
if (!currentActiveViewport) {
return null; // Return an appropriate value if the current active viewport is not found
}
// for the new viewports, we should rank them by the displaySetInstanceUIDs
// they are displaying and the orientation then we can find the active viewport
const currentActiveDisplaySetInstanceUIDs =
currentActiveViewport.displaySetInstanceUIDs;
// This doesn't take into account where stack viewport is converting to volumeViewport
// since in stack viewport we don't have a concept of "orientation" as a string
// maybe we should calculate the orientation based on the active imageId
// so that we can compare it with the new viewports (which might be volume viewports)
// and find the best match
const currentOrientation = currentActiveViewport.viewportOptions.orientation;
const sortedViewports = Array.from(newViewports.values()).sort((a, b) => {
// Compare orientations
const aOrientationMatch =
a.viewportOptions.orientation === currentOrientation;
const bOrientationMatch =
b.viewportOptions.orientation === currentOrientation;
if (aOrientationMatch !== bOrientationMatch) {
return bOrientationMatch - aOrientationMatch;
}
// Compare displaySetInstanceUIDs
const aMatch = a.displaySetInstanceUIDs.some(uid =>
currentActiveDisplaySetInstanceUIDs.includes(uid)
);
const bMatch = b.displaySetInstanceUIDs.some(uid =>
currentActiveDisplaySetInstanceUIDs.includes(uid)
);
if (aMatch !== bMatch) {
return bMatch - aMatch;
}
return 0; // Return 0 if no differences found
});
if (!sortedViewports?.length) {
return null;
}
return sortedViewports[0].viewportId;
};
export const ViewportGridContext = createContext(DEFAULT_STATE);
/** A viewport is reuseable if it is the same size as the old
* one and has the same display sets, in the same position.
* It SHOULD be possible to re-use them at different positions, but
* this causes problems with segmentation.
*/
const isReuseableViewport = (oldViewport, newViewport) => {
const sameDiplaySets = isEqual(
oldViewport.displaySetInstanceUIDs,
newViewport.displaySetInstanceUIDs
);
return (
oldViewport.viewportIndex === newViewport.viewportIndex &&
sameDiplaySets &&
oldViewport.height === newViewport.height &&
oldViewport.width === newViewport.width
);
};
// Holds a global viewport counter - used to assign new id's to viewports
// Starts at a value above zero so that any of the old viewport id's are
// immediately obvious and if we get any index generated viewports, they are
// definitely distinct from these ones.
let viewportCounter = 5000;
/**
* Find a viewportId to re-use if possible, preserving the existing
* viewport information, OR create a new one if the viewport given isn't
* compatible with what was there before.
*
* @param viewportIdSet
* @param viewport
* @param stateViewports
* @returns
*/
const reuseViewportId = (viewportIdSet: Set, viewport, stateViewports) => {
for (const oldViewport of stateViewports) {
const { viewportId: oldId } = oldViewport;
if (!oldId) {
// This occurs on startup, so skip re-using it
continue;
}
if (viewportIdSet.has(oldId)) {
// oldId is already used - we can't reuse it
continue;
}
if (isReuseableViewport(oldViewport, viewport)) {
viewportIdSet.add(oldId);
// This means the old and the new viewport are compatible, and
// since we have gotten here, the viewport ID isn't used, so we
// are good to reuse it.
// This will remember the old viewport options, assuming they are unchanging.
return {
...oldViewport,
...viewport,
id: oldId,
viewportId: oldId,
viewportOptions: {
// Update any viewport options from new
...viewport.viewportOptions,
viewportId: oldId,
},
};
}
}
// There wasn't an old id found to be reused, so create a new one
// Find a viewport instance number different from earlier viewports
const viewportId = 'viewport-' + viewportCounter;
viewportIdSet.add(viewportId);
// Loop over viewport counters in case of a really long lived display
viewportCounter = (viewportCounter + 1) % 100000;
// viewportOptions is already a copy, so can just update direct
viewport.viewportOptions.viewportId = viewportId;
return {
...viewport,
id: viewportId,
viewportId,
};
};
export function ViewportGridProvider({ children, service }) {
const viewportGridReducer = (state, action) => {
const viewportGridReducer = (state: DefaultState, action) => {
switch (action.type) {
case 'SET_ACTIVE_VIEWPORT_INDEX': {
return { ...state, ...{ activeViewportIndex: action.payload } };
case 'SET_ACTIVE_VIEWPORT_ID': {
return { ...state, ...{ activeViewportId: action.payload } };
}
/**
@@ -130,65 +139,49 @@ export function ViewportGridProvider({ children, service }) {
*/
case 'SET_DISPLAYSETS_FOR_VIEWPORTS': {
const { payload } = action;
const viewports = state.viewports.slice();
const viewports = new Map(state.viewports);
// Have the initial id set contain all viewports not updated here
const viewportIdSet = new Set();
viewports.forEach((viewport, index) => {
if (!viewport.viewportId) {
return;
}
const isUpdated = payload.find(
newViewport => newViewport.viewportIndex === index
);
if (isUpdated) {
return;
}
viewportIdSet.add(viewport.viewportId);
});
payload.forEach(updatedViewport => {
const { viewportId, displaySetInstanceUIDs } = updatedViewport;
const previousViewport = viewports.get(viewportId);
for (const updatedViewport of payload) {
// Use the newly provide viewportOptions and display set options
// when provided, and otherwise fall back to the previous ones.
// That allows for easy updates of just the display set.
const { viewportIndex, displaySetInstanceUIDs } = updatedViewport;
const previousViewport = viewports[viewportIndex] || {};
const viewportOptions = {
...(updatedViewport.viewportOptions ||
previousViewport.viewportOptions),
};
const viewportOptions = merge(
{},
updatedViewport?.viewportOptions,
previousViewport?.viewportOptions
);
const displaySetOptions = updatedViewport.displaySetOptions || [];
if (!displaySetOptions.length) {
// Copy all the display set options, assuming a full set of displa
// set UID's is provided.
// Copy all the display set options, assuming a full set of displaySet UID's is provided.
displaySetOptions.push(...previousViewport.displaySetOptions);
if (!displaySetOptions.length) {
displaySetOptions.push({});
}
}
let newViewport = {
const newViewport = {
...previousViewport,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
viewportLabel: viewportLabels[viewportIndex],
viewportLabel: getViewportLabel(viewports, viewportId),
};
viewportOptions.presentationIds = ViewportGridService.getPresentationIds(
newViewport,
viewports
);
newViewport = reuseViewportId(
viewportIdSet,
newViewport,
state.viewports
);
newViewport.viewportIndex = previousViewport.viewportIndex;
viewports[viewportIndex] = newViewport;
}
viewports.set(viewportId, {
...viewports.get(viewportId),
...newViewport,
});
});
return { ...state, viewports };
}
@@ -198,13 +191,13 @@ export function ViewportGridProvider({ children, service }) {
numRows,
layoutOptions,
layoutType = 'grid',
activeViewportIndex,
activeViewportId,
findOrCreateViewport,
} = action.payload;
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
const hasOptions = layoutOptions?.length;
const viewports = [];
const viewports = new Map<string, Viewport>();
// Options is a temporary state store which can be used by the
// findOrCreate to store state about already found viewports. Typically,
@@ -213,34 +206,46 @@ export function ViewportGridProvider({ children, service }) {
// haven't been viewed yet, and add them in the appropriate order.
const options = {};
let activeViewportIndexToSet = activeViewportIndex;
let activeViewportIdToSet = activeViewportId;
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
const pos = col + row * numCols;
const layoutOption = layoutOptions[pos];
const position = col + row * numCols;
const layoutOption = layoutOptions[position];
const positionId = layoutOption?.positionId || `${col}-${row}`;
if (hasOptions && pos >= layoutOptions.length) {
if (hasOptions && position >= layoutOptions.length) {
continue;
}
if (
activeViewportIndexToSet == null &&
state.viewports[state.activeViewportIndex]?.positionId ===
positionId
) {
activeViewportIndexToSet = pos;
}
const viewport = findOrCreateViewport(pos, positionId, options);
const viewport = findOrCreateViewport(
position,
positionId,
options
);
if (!viewport) {
continue;
}
viewport.positionId = positionId;
// If the viewport doesn't have a viewportId, we create one
if (!viewport.viewportOptions?.viewportId) {
const randomUID = utils.uuidv4().substring(0, 8);
viewport.viewportOptions.viewportId = `viewport-${randomUID}`;
}
viewport.viewportId = viewport.viewportOptions.viewportId;
// 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;
viewports.set(viewport.viewportId, viewport);
if (layoutOptions && layoutOptions[pos]) {
({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[pos]);
let xPos, yPos, w, h;
if (layoutOptions && layoutOptions[position]) {
({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[
position
]);
} else {
w = 1 / numCols;
h = 1 / numRows;
@@ -248,40 +253,35 @@ export function ViewportGridProvider({ children, service }) {
yPos = row * h;
}
viewport.width = w;
viewport.height = h;
viewport.x = xPos;
viewport.y = yPos;
}
}
Object.assign(viewport, {
width: w,
height: h,
x: xPos,
y: yPos,
});
activeViewportIndexToSet = activeViewportIndexToSet ?? 0;
const viewportIdSet = new Set();
for (
let viewportIndex = 0;
viewportIndex < viewports.length;
viewportIndex++
) {
const viewport = reuseViewportId(
viewportIdSet,
viewports[viewportIndex],
state.viewports
);
if (!viewport.viewportOptions.presentationIds) {
viewport.viewportOptions.presentationIds = ViewportGridService.getPresentationIds(
viewport,
viewports
viewport.viewportLabel = getViewportLabel(
viewports,
viewport.viewportId
);
if (!viewport.viewportOptions.presentationIds) {
viewport.viewportOptions.presentationIds = ViewportGridService.getPresentationIds(
viewport,
viewports
);
}
}
viewport.viewportIndex = viewportIndex;
viewport.viewportLabel = viewportLabels[viewportIndex];
viewports[viewportIndex] = viewport;
}
activeViewportIdToSet =
activeViewportIdToSet ??
determineActiveViewportId(state, viewports) ??
'default';
const ret = {
...state,
activeViewportIndex: activeViewportIndexToSet,
activeViewportId: activeViewportIdToSet,
layout: {
...state.layout,
numCols,
@@ -317,8 +317,8 @@ export function ViewportGridProvider({ children, service }) {
return viewportGridState;
}, [viewportGridState]);
const setActiveViewportIndex = useCallback(
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }),
const setActiveViewportId = useCallback(
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }),
[dispatch]
);
@@ -337,7 +337,7 @@ export function ViewportGridProvider({ children, service }) {
numRows,
numCols,
layoutOptions = [],
activeViewportIndex,
activeViewportId,
findOrCreateViewport,
}) =>
dispatch({
@@ -347,7 +347,7 @@ export function ViewportGridProvider({ children, service }) {
numRows,
numCols,
layoutOptions,
activeViewportIndex,
activeViewportId,
findOrCreateViewport,
},
}),
@@ -375,7 +375,7 @@ export function ViewportGridProvider({ children, service }) {
const getNumViewportPanes = useCallback(() => {
const { layout, viewports } = viewportGridState;
const { numRows, numCols } = layout;
return Math.min(viewports.length, numCols * numRows);
return Math.min(viewports.size, numCols * numRows);
}, [viewportGridState]);
/**
@@ -387,7 +387,7 @@ export function ViewportGridProvider({ children, service }) {
if (service) {
service.setServiceImplementation({
getState,
setActiveViewportIndex,
setActiveViewportId,
setDisplaySetsForViewports,
setLayout,
reset,
@@ -399,7 +399,7 @@ export function ViewportGridProvider({ children, service }) {
}, [
getState,
service,
setActiveViewportIndex,
setActiveViewportId,
setDisplaySetsForViewports,
setLayout,
reset,
@@ -410,7 +410,7 @@ export function ViewportGridProvider({ children, service }) {
// run many of the calls through the service itself since we want to publish events
const api = {
getState,
setActiveViewportIndex: index => service.setActiveViewportIndex(index),
setActiveViewportId: index => service.setActiveViewportId(index),
setDisplaySetsForViewport: props =>
service.setDisplaySetsForViewports([props]),
setDisplaySetsForViewports: props =>