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

@@ -16,12 +16,18 @@ describe('OHIF Double Click', () => {
.should('be.eq', numExpectedViewports);
for (let i = 0; i < numExpectedViewports; i += 1) {
cy.wait(2000);
// For whatever reason, with Cypress tests, we have to activate the
// viewport we are double clicking first.
cy.get('[data-cy="viewport-pane"]')
.eq(i)
.trigger('mousedown', 'center', { force: true })
.trigger('mouseup', 'center', { force: true });
.trigger('mousedown', 'center', {
force: true,
})
.trigger('mouseup', 'center', {
force: true,
});
// Wait for the viewport to be 'active'.
// TODO Is there a better way to do this?
@@ -41,12 +47,16 @@ describe('OHIF Double Click', () => {
.should('be.eq', 1);
cy.get('[data-cy="viewport-pane"]')
.eq(0)
.trigger('dblclick', 'center');
.trigger('mousedown', 'center', {
force: true,
})
.trigger('mouseup', 'center', {
force: true,
});
cy.get('[data-cy="viewport-pane"]')
.its('length')
.should('be.eq', numExpectedViewports);
.eq(0)
.trigger('dblclick', 'center');
}
});
});
@@ -66,9 +66,10 @@ describe('OHIF MPR', () => {
.then(cornerstone => {
const viewports = cornerstone.getRenderingEngines()[0].getViewports();
const imageData1 = viewports[0].getImageData();
const imageData2 = viewports[1].getImageData();
const imageData3 = viewports[2].getImageData();
// The stack viewport still exists after the changes to viewportId and inde
const imageData1 = viewports[1].getImageData();
const imageData2 = viewports[2].getImageData();
const imageData3 = viewports[3].getImageData();
// for some reason map doesn't work here
cy.wrap(imageData1).should('not.be', undefined);
+36 -31
View File
@@ -10,7 +10,7 @@ function ViewerViewportGrid(props) {
const { servicesManager, viewportComponents, dataSource } = props;
const [viewportGrid, viewportGridService] = useViewportGrid();
const { layout, activeViewportIndex, viewports } = viewportGrid;
const { layout, activeViewportId, viewports } = viewportGrid;
const { numCols, numRows } = layout;
// TODO -> Need some way of selecting which displaySets hit the viewports.
@@ -51,10 +51,11 @@ function ViewerViewportGrid(props) {
* specify the viewport match details, which specifies the size and
* setup of the various viewports.
*/
const findOrCreateViewport = viewportIndex => {
const details = viewportMatchDetails.get(viewportIndex);
const findOrCreateViewport = pos => {
const viewportId = Array.from(viewportMatchDetails.keys())[pos];
const details = viewportMatchDetails.get(viewportId);
if (!details) {
console.log('No match details for viewport', viewportIndex);
console.log('No match details for viewport', viewportId);
return;
}
@@ -99,11 +100,11 @@ function ViewerViewportGrid(props) {
};
const _getUpdatedViewports = useCallback(
(viewportIndex, displaySetInstanceUID) => {
(viewportId, displaySetInstanceUID) => {
let updatedViewports = [];
try {
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
viewportId,
displaySetInstanceUID
);
} catch (error) {
@@ -144,7 +145,7 @@ function ViewerViewportGrid(props) {
useEffect(() => {
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
({ viewportIndex, measurement, isConsumed }) => {
({ viewportId, measurement, isConsumed }) => {
if (isConsumed) {
return;
}
@@ -155,7 +156,7 @@ function ViewerViewportGrid(props) {
measurement;
const updatedViewports = _getUpdatedViewports(
viewportIndex,
viewportId,
referencedDisplaySetInstanceUID
);
// Arbitrarily assign the viewport to element 0
@@ -197,7 +198,7 @@ function ViewerViewportGrid(props) {
}, [viewports]);
/**
const onDoubleClick = viewportIndex => {
const onDoubleClick = viewportId => {
// TODO -> Disabled for now.
// onNewImage on a cornerstone viewport is firing setDisplaySetsForViewport.
// Which it really really shouldn't. We need a larger fix for jump to
@@ -206,7 +207,7 @@ function ViewerViewportGrid(props) {
viewportGridService.set({
numCols: cachedLayout.numCols,
numRows: cachedLayout.numRows,
activeViewportIndex: cachedLayout.activeViewportIndex,
activeViewportId: cachedLayout.activeViewportId,
viewports: cachedLayout.viewports,
cachedLayout: null,
});
@@ -223,10 +224,10 @@ function ViewerViewportGrid(props) {
viewportGridService.set({
numCols: 1,
numRows: 1,
activeViewportIndex: 0,
activeViewportId: 0,
viewports: [
{
displaySetInstanceUID: viewports[viewportIndex].displaySetInstanceUID,
displaySetInstanceUID: viewports[viewportId].displaySetInstanceUID,
imageIndex: undefined,
},
],
@@ -234,15 +235,15 @@ function ViewerViewportGrid(props) {
numCols,
numRows,
viewports: cachedViewports,
activeViewportIndex: viewportIndex,
activeViewportId: viewportId,
},
});
};
*/
const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => {
const onDropHandler = (viewportId, { displaySetInstanceUID }) => {
const updatedViewports = _getUpdatedViewports(
viewportIndex,
viewportId,
displaySetInstanceUID
);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
@@ -253,13 +254,7 @@ function ViewerViewportGrid(props) {
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 paneMetadata = Array.from(viewports.values())[i] || {};
const {
displaySetInstanceUIDs,
viewportOptions,
@@ -271,9 +266,13 @@ function ViewerViewportGrid(props) {
viewportLabel,
} = paneMetadata;
const viewportId = viewportOptions.viewportId
const isActive = activeViewportId === viewportId;
const displaySetInstanceUIDsToUse = displaySetInstanceUIDs || [];
// This is causing the viewport components re-render when the activeViewportIndex changes
// This is causing the viewport components re-render when the activeViewportId changes
const displaySets = displaySetInstanceUIDsToUse.map(
displaySetInstanceUID => {
return (
@@ -307,17 +306,23 @@ function ViewerViewportGrid(props) {
event.stopPropagation();
}
viewportGridService.setActiveViewportIndex(viewportIndex);
viewportGridService.setActiveViewportId(viewportId);
};
// TEMP -> Double click disabled for now
// onDoubleClick={() => onDoubleClick(viewportIndex)}
viewportPanes[i] = (
<ViewportPane
// Note: It is highly important that the key is the viewportId here,
// since it is used to determine if the component should be re-rendered
// by React, and also in the hanging protocol and stage changes if the
// same viewportId is used, React, by default, will only move (not re-render)
// those components. For instance, if we have a 2x3 layout, and we move
// from 2x3 to 1x1 (second viewport), if the key is the viewportIndex,
// React will RE-RENDER the resulting viewport as the key will be different.
// however, if the key is the viewportId, React will only move the component
// and not re-render it.
key={viewportId}
acceptDropsFor="displayset"
onDrop={onDropHandler.bind(null, viewportIndex)}
onDrop={onDropHandler.bind(null, viewportId)}
onInteraction={onInteractionHandler}
customStyle={{
position: 'absolute',
@@ -336,8 +341,8 @@ function ViewerViewportGrid(props) {
>
<ViewportComponent
displaySets={displaySets}
viewportIndex={viewportIndex}
viewportLabel={viewports.length > 1 ? viewportLabel : ''}
viewportLabel={viewports.size > 1 ? viewportLabel : ''}
viewportId={viewportId}
dataSource={dataSource}
viewportOptions={viewportOptions}
displaySetOptions={displaySetOptions}
@@ -349,7 +354,7 @@ function ViewerViewportGrid(props) {
}
return viewportPanes;
}, [viewports, activeViewportIndex, viewportComponents, dataSource]);
}, [viewports, activeViewportId, viewportComponents, dataSource]);
/**
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
@@ -74,13 +74,14 @@ const match = (
errorMessages = ['Something went wrong during validation.', e];
}
console.log(
'Test',
`${from}.${attribute}`,
readValues[attribute],
JSON.stringify(rule.constraint),
!errorMessages
);
// TODO: move to a logger
// console.log(
// 'Test',
// `${from}.${attribute}`,
// readValues[attribute],
// JSON.stringify(rule.constraint),
// !errorMessages
// );
if (!errorMessages) {
// If no errorMessages were returned, then validation passed.
@@ -118,7 +118,7 @@ function checkHpsBestMatch(hps) {
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
const { viewportMatchDetails } = hps.getMatchDetails();
expect(viewportMatchDetails.size).toBe(1);
expect(viewportMatchDetails.get(0)).toMatchObject({
expect(viewportMatchDetails.get('ctAXIAL')).toMatchObject({
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
@@ -14,6 +14,7 @@ import {
} from './custom-attribute/isDisplaySetFromUrl';
import numberOfDisplaySetsWithImages from './custom-attribute/numberOfDisplaySetsWithImages';
import seriesDescriptionsFromDisplaySets from './custom-attribute/seriesDescriptionsFromDisplaySets';
import uuidv4 from '../../utils/uuidv4';
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
@@ -132,11 +133,11 @@ export default class HangingProtocolService extends PubSubService {
> = new Map();
/**
* An array that contains for each viewport (viewportIndex) specified in the
* An array that contains for each viewport (viewportId) specified in the
* hanging protocol, an object of the form
*/
viewportMatchDetails: Map<
number, // viewportIndex
string, // viewportId
HangingProtocol.ViewportMatchDetails
> = new Map();
@@ -185,7 +186,7 @@ export default class HangingProtocolService extends PubSubService {
stage: HangingProtocol.ProtocolStage;
stageIndex: number;
activeStudy?: StudyMetadata;
viewportMatchDetails: Map<number, HangingProtocol.ViewportMatchDetails>;
viewportMatchDetails: Map<string, HangingProtocol.ViewportMatchDetails>;
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>;
activeImageLoadStrategyName: string;
} {
@@ -404,7 +405,7 @@ export default class HangingProtocolService extends PubSubService {
/**
* Returns true, if the hangingProtocol has a custom loading strategy for the images
* and its callback has been added to the HangingProtocolService
* @returns {boolean} true
* @returns A boolean indicating whether a custom image load strategy has been added or not.
*/
public hasCustomImageLoadStrategy(): boolean {
return (
@@ -415,10 +416,31 @@ export default class HangingProtocolService extends PubSubService {
);
}
/**
* Returns a boolean indicating whether a custom image load has been performed or not.
* A custom image load is performed when a custom image load strategy is used to load images.
* This method is used internally by the HangingProtocolService to determine whether to perform
* a custom image load or not.
*
* @returns A boolean indicating whether a custom image load has been performed or not.
*/
public getCustomImageLoadPerformed(): boolean {
return this.customImageLoadPerformed;
}
/**
* Returns a boolean indicating whether a custom image load should be performed or not.
* A custom image load should be performed if a custom image load strategy has been added to the HangingProtocolService
* and it has not been performed yet.
*
* @returns A boolean indicating whether a custom image load should be performed or not.
*/
public getShouldPerformCustomImageLoad(): boolean {
return (
this.hasCustomImageLoadStrategy() && !this.getCustomImageLoadPerformed()
);
}
/**
* Set the strategy callback for loading images to the HangingProtocolService
* @param {string} name strategy name
@@ -519,21 +541,28 @@ export default class HangingProtocolService extends PubSubService {
for (let i = 0; i < rows * columns; i++) {
stage.viewports.push({
viewportOptions: defaultViewportOptions,
viewportOptions: {
...defaultViewportOptions,
viewportId: uuidv4(),
},
displaySets: [],
});
}
} else {
// Clone each viewport to ensure independent objects
stage.viewports = stage.viewports.map(viewport => ({
...viewport,
viewportOptions: {
...(viewport.viewportOptions || defaultViewportOptions),
viewportId: viewport.viewportOptions?.viewportId || uuidv4(),
},
displaySets: viewport.displaySets || [],
}));
stage.viewports.forEach(viewport => {
viewport.viewportOptions =
viewport.viewportOptions || defaultViewportOptions;
if (!viewport.displaySets) {
viewport.displaySets = [];
} else {
viewport.displaySets.forEach(displaySet => {
displaySet.options = displaySet.options || {};
});
}
viewport.displaySets.forEach(displaySet => {
displaySet.options = displaySet.options || {};
});
});
}
});
@@ -558,7 +587,7 @@ export default class HangingProtocolService extends PubSubService {
};
}
getViewportsRequireUpdate(viewportIndex, displaySetInstanceUID) {
getViewportsRequireUpdate(viewportId, displaySetInstanceUID) {
const { displaySetService } = this._servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
@@ -570,15 +599,22 @@ export default class HangingProtocolService extends PubSubService {
const protocol = this.protocol;
const protocolStage = protocol.stages[this.stageIndex];
const protocolViewports = protocolStage.viewports;
const protocolViewport = protocolViewports[viewportIndex];
const defaultReturn = [
{
viewportIndex,
viewportId,
displaySetInstanceUIDs: [newDisplaySetInstanceUID],
},
];
if (!protocolViewports) {
return defaultReturn;
}
const protocolViewport = protocolViewports.find(
pv => pv.viewportOptions.viewportId === viewportId
);
// if no viewport, then we can assume there is no predefined set of
// rules that should be applied to this viewport while matching
if (!protocolViewport) {
@@ -644,13 +680,13 @@ export default class HangingProtocolService extends PubSubService {
}
// if we reach here, it means that the displaySetInstanceUIDs to be dropped
// in the viewportIndex are valid, and we can proceed with the update. However
// for the desired viewportId are valid, and we can proceed with the update. However
// we need to check if the displaySets that the viewport were showing
// was also referenced by other viewports, and if so, we need to update those
// viewports as well
// check if displaySetSelectors are used by other viewports, and
// store the viewportIndex and displaySetInstanceUIDs that need to be updated
// store the viewportId and displaySetInstanceUIDs that need to be updated
const viewportsToUpdate = [];
protocolViewports.forEach((viewport, index) => {
const viewportNeedsUpdate = viewport.displaySets.some(
@@ -661,7 +697,7 @@ export default class HangingProtocolService extends PubSubService {
if (viewportNeedsUpdate) {
// Try to recompute the viewport options based on the current
// viewportIndex that needs update but from its old/original un-computed
// viewportId that needs update but from its old/original un-computed
// viewport & displaySet options
if (originalProtocolStage) {
const originalViewport = originalProtocolStage.viewports[index];
@@ -692,7 +728,7 @@ export default class HangingProtocolService extends PubSubService {
);
viewportsToUpdate.push({
viewportIndex: index,
viewportId: viewport.viewportOptions.viewportId,
displaySetInstanceUIDs,
viewportOptions: viewport.viewportOptions,
displaySetOptions,
@@ -799,7 +835,7 @@ export default class HangingProtocolService extends PubSubService {
* @param protocolId - name of the registered protocol to be set
* @param options - options to be passed to the protocol, this is either an array
* of the displaySetInstanceUIDs to be set on ALL VIEWPORTS OF THE PROTOCOL or an object
* that contains viewportIndex as the key and displaySetInstanceUIDs as the value
* that contains viewportId as the key and displaySetInstanceUIDs as the value
* for each viewport that needs to be set.
* @param errorCallback - callback to be called if there is an error
* during the protocol application
@@ -918,7 +954,7 @@ export default class HangingProtocolService extends PubSubService {
const stage = stages[i];
if (stage.id === stageId && stage.status !== 'disabled') {
return i;
}
}
}
return;
}
@@ -1098,7 +1134,6 @@ export default class HangingProtocolService extends PubSubService {
// matching applied
this.viewportMatchDetails = new Map();
this.displaySetMatchDetails = new Map();
this.customImageLoadPerformed = false;
// Retrieve the current stage
const stageModel = this._getCurrentStageModel();
@@ -1148,7 +1183,11 @@ export default class HangingProtocolService extends PubSubService {
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>;
} {
let matchedViewports = 0;
stageModel.viewports.forEach((viewport, viewportIndex) => {
stageModel.viewports.forEach(viewport => {
// Todo: we should probably assign a random viewportId if not defined
// below, but it feels odd since viewportGrid should handle this kind
// of thing
const viewportId = viewport.viewportOptions.viewportId;
const matchDetails = this._matchViewport(
viewport,
options,
@@ -1169,7 +1208,7 @@ export default class HangingProtocolService extends PubSubService {
displaySetOptions: it,
}));
}
viewportMatchDetails.set(viewportIndex, matchDetails);
viewportMatchDetails.set(viewportId, matchDetails);
}
});
return {
@@ -1231,7 +1270,7 @@ export default class HangingProtocolService extends PubSubService {
for (const subMatch of match.matchingScores) {
if (subMatch.displaySetInstanceUID === displaySetUID) {
return;
}
}
}
throw new Error(
`Reused viewport details ${id} with ds ${displaySetUID} not valid`
@@ -1442,15 +1481,15 @@ export default class HangingProtocolService extends PubSubService {
);
const studyMatchDetails = this.protocolEngine.findMatch(
study,
studyMatchingRules,
{
studies: this.studies,
displaySets: studyDisplaySets,
allDisplaySets: this.displaySets,
displaySetMatchDetails: this.displaySetMatchDetails,
study,
studyMatchingRules,
{
studies: this.studies,
displaySets: studyDisplaySets,
allDisplaySets: this.displaySets,
displaySetMatchDetails: this.displaySetMatchDetails,
studyInstanceUIDsIndex,
}
}
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
@@ -1459,10 +1498,10 @@ export default class HangingProtocolService extends PubSubService {
}
this.debug(
'study',
study.StudyInstanceUID,
'display sets #',
studyDisplaySets.length
'study',
study.StudyInstanceUID,
'display sets #',
studyDisplaySets.length
);
studyDisplaySets.forEach(displaySet => {
const {
@@ -1471,15 +1510,15 @@ export default class HangingProtocolService extends PubSubService {
displaySetInstanceUID,
} = displaySet;
const seriesMatchDetails = this.protocolEngine.findMatch(
displaySet,
seriesMatchingRules,
// Todo: why we have images here since the matching type does not have it
{
studies: this.studies,
instance: displaySet.images?.[0],
displaySetMatchDetails: this.displaySetMatchDetails,
displaySets: studyDisplaySets,
}
displaySet,
seriesMatchingRules,
// Todo: why we have images here since the matching type does not have it
{
studies: this.studies,
instance: displaySet.images?.[0],
displaySetMatchDetails: this.displaySetMatchDetails,
displaySets: studyDisplaySets,
}
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
@@ -649,10 +649,7 @@ class MeasurementService extends PubSubService {
* merely causes it to fire the event with the isConsumed set to true.
*/
public jumpToMeasurement(
viewportIndex: number,
measurementUID: string
): void {
public jumpToMeasurement(viewportId: string, measurementUID: string): void {
const measurement = this.measurements.get(measurementUID);
if (!measurement) {
@@ -660,7 +657,7 @@ class MeasurementService extends PubSubService {
return;
}
const consumableEvent = this.createConsumableEvent({
viewportIndex,
viewportId,
measurement,
});
@@ -4,7 +4,7 @@
* @typedef {Object} ViewportDialogProps
* @property {ReactElement|HTMLElement} [content=null] Modal content.
* @property {Object} [contentProps=null] Modal content props.
* @property {boolean} [viewportIndex=false] Modal is dismissible via the esc key.
* @property {boolean} [viewportId=false] Modal is dismissible via the esc key.
*/
const name = 'uiViewportDialogService';
@@ -22,12 +22,12 @@ const serviceImplementation = {
};
/**
* Show a new UI viewport dialog on the specified viewportIndex;
* Show a new UI viewport dialog on the specified viewportId;
*
* @param {ViewportDialogProps} props { content, contentProps, viewportIndex }
* @param {ViewportDialogProps} props { content, contentProps, viewportId }
*/
function _show({
viewportIndex,
viewportId,
id,
type,
message,
@@ -36,7 +36,7 @@ function _show({
onOutsideClick,
}) {
return serviceImplementation._show({
viewportIndex,
viewportId,
id,
type,
message,
@@ -59,7 +59,7 @@ function _hide() {
* @param {*} {
* hide: hideImplementation,
* show: showImplementation,
* viewportIndex,
* viewportId,
* }
*/
function setServiceImplementation({
@@ -3,7 +3,7 @@ import { getPresentationIds, PresentationIds } from './getPresentationIds';
class ViewportGridService extends PubSubService {
public static readonly EVENTS = {
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
ACTIVE_VIEWPORT_ID_CHANGED: 'event::activeviewportidchanged',
LAYOUT_CHANGED: 'event::layoutChanged',
GRID_STATE_CHANGED: 'event::gridStateChanged',
};
@@ -27,7 +27,7 @@ class ViewportGridService extends PubSubService {
public setServiceImplementation({
getState: getStateImplementation,
setActiveViewportIndex: setActiveViewportIndexImplementation,
setActiveViewportId: setActiveViewportIdImplementation,
setDisplaySetsForViewports: setDisplaySetsForViewportsImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
@@ -38,9 +38,9 @@ class ViewportGridService extends PubSubService {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setActiveViewportIndexImplementation) {
this.serviceImplementation._setActiveViewportIndex =
setActiveViewportIndexImplementation;
if (setActiveViewportIdImplementation) {
this.serviceImplementation._setActiveViewport =
setActiveViewportIdImplementation;
}
if (setDisplaySetsForViewportsImplementation) {
this.serviceImplementation._setDisplaySetsForViewports =
@@ -64,13 +64,11 @@ class ViewportGridService extends PubSubService {
}
}
public setActiveViewportIndex(index) {
this.serviceImplementation._setActiveViewportIndex(index);
public setActiveViewportId(id: string) {
this.serviceImplementation._setActiveViewport(id);
const state = this.getState();
const viewportId = state.viewports[index]?.viewportOptions?.viewportId;
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED, {
viewportIndex: index,
viewportId,
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
viewportId: id,
});
}
@@ -89,7 +87,7 @@ class ViewportGridService extends PubSubService {
const viewports = [];
for (const viewport of props) {
const updatedViewport = state.viewports[viewport.viewportIndex];
const updatedViewport = state.viewports.get(viewport.viewportId);
if (updatedViewport) {
viewports.push(updatedViewport);
} else {
@@ -118,7 +116,7 @@ class ViewportGridService extends PubSubService {
numRows,
layoutOptions,
layoutType = 'grid',
activeViewportIndex = undefined,
activeViewportId = undefined,
findOrCreateViewport = undefined,
}) {
this.serviceImplementation._setLayout({
@@ -126,7 +124,7 @@ class ViewportGridService extends PubSubService {
numRows,
layoutOptions,
layoutType,
activeViewportIndex,
activeViewportId,
findOrCreateViewport,
});
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
@@ -160,8 +158,8 @@ class ViewportGridService extends PubSubService {
return this.serviceImplementation._getNumViewportPanes();
}
public getLayoutOptionsFromState(state) {
return state.viewports.map(viewport => {
public getLayoutOptionsFromState(state: any): { x: number; y: number; width: number; height: number }[] {
return Array.from(state.viewports.entries()).map(([_, viewport]) => {
return {
x: viewport.x,
y: viewport.y,
@@ -25,7 +25,7 @@ const addUniqueIndex = (arr, key, viewports, isUpdatingSameViewport) => {
arr[arr.length - 1] = displayInstance;
const testId = arr.join(JOIN_STR);
if (
!viewports.find(
!Array.from(viewports.values()).find(
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
)
) {
@@ -126,11 +126,11 @@ const getPresentationIds = (viewport, viewports): PresentationIds => {
}
// only add unique index if the viewport is getting inserted and not updated
const isUpdatingSameViewport = viewports.some(v => {
const isUpdatingSameViewport = Array.from(viewports.values()).some(v => {
return (
v.displaySetInstanceUIDs.toString() ===
viewport.displaySetInstanceUIDs.toString() &&
v.viewportIndex === viewport.viewportIndex
v.viewportId === viewport.viewportId
);
});
+3 -1
View File
@@ -61,7 +61,7 @@ export type SetProtocolOptions = {
export type HangingProtocolMatchDetails = {
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
viewportMatchDetails: Map<number, ViewportMatchDetails>;
viewportMatchDetails: Map<string, ViewportMatchDetails>;
};
export type ConstraintValue =
@@ -263,6 +263,8 @@ export type ProtocolNotifications = {
export type Protocol = {
// Mandatory
id: string;
/** A description of this protocol. Used as a tool tip for the user. */
description?: string;
/** Maps ids to display set selectors to choose display sets */
displaySetSelectors: Record<string, DisplaySetSelector>;
/** A default viewport to use for any stage to select new viewport layouts. */
+2
View File
@@ -1,6 +1,7 @@
import ObjectPath from './objectPath';
import absoluteUrl from './absoluteUrl';
import guid from './guid';
import uuidv4 from './uuidv4';
import sortBy from './sortBy.js';
import writeScript from './writeScript.js';
import b64toBlob from './b64toBlob.js';
@@ -41,6 +42,7 @@ import { splitComma, getSplitParam } from './splitComma';
const utils = {
guid,
uuidv4,
ObjectPath,
absoluteUrl,
sortBy,
+1
View File
@@ -39,6 +39,7 @@ describe('Top level exports', () => {
'hierarchicalListUtils',
'progressTrackingUtils',
'subscribeToNextViewportGridChange',
'uuidv4',
].sort();
const exports = Object.keys(utils.default).sort();
@@ -1,7 +1,7 @@
import { ViewportGridService } from '../services';
/**
* Subscribes to the very next LAYOUT_CHANGED or GRID_STATE_CHANGED event that
* Subscribes to the very next LAYOUT_CHANGED event that
* is not currently on the event queue. The subscriptions are made on a 'zero'
* timeout so as to avoid responding to any of those events currently on the event queue.
* The subscription persists only for a single invocation of either event.
@@ -24,10 +24,6 @@ function subscribeToNextViewportGridChange(
viewportGridService.EVENTS.LAYOUT_CHANGED,
callback
),
viewportGridService.subscribe(
viewportGridService.EVENTS.GRID_STATE_CHANGED,
callback
),
];
};
@@ -43,20 +43,13 @@ const getViewportModule = () => {
A simplified version of the tracked `OHIFCornerstoneViewport` is shown below, which
creates a cornerstone viewport:
:::note Tip
Not in OHIF version 3.1 we use `displaySets` in the props which is new compared to
the previous version (3.0) which uses `displaySet`. This is due to the fact that
we are moving to a new data model that can render fused images in a single viewport.
:::
```jsx
function TrackedCornerstoneViewport({
children,
dataSource,
displaySets,
viewportIndex,
viewportId,
servicesManager,
extensionManager,
commandsManager,
@@ -87,6 +80,45 @@ function TrackedCornerstoneViewport({
}
```
### Viewport re-rendering optimizations
We make use of the React memoization pattern to prevent unnecessary re-renders
for the viewport unless certain aspects of the Viewport props change. You can take
a look into the `areEqual` function in the `OHIFCornerstoneViewport` component to
see how this is done.
```js
function areEqual(prevProps, nextProps) {
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
if (
prevProps.viewportOptions.orientation !==
nextProps.viewportOptions.orientation
) {
return false;
}
// rest of the code
```
as you see, we check if the `needsRerendering` prop is true, and if so, we will
re-render the viewport if the `displaySets` prop changes or the orientation
changes.
We use viewportId to identify a viewport and we use it as a key in React
rendering. This is important because it allows us to keep track of the viewport
and its state, and also let React optimize and move the viewport around in the
grid without re-rendering it. However, there are some cases where we need to
force re-render the viewport, for example, when the viewport is hydrated
with a new Segmentation. For these cases, we use the `needsRerendering` prop
to force re-render the viewport. You can add it to the `viewportOptions`
### `@ohif/app`
@@ -68,7 +68,7 @@ There are seven events that get publish in `MeasurementService`:
- `toMeasurementSchema`: A function to get the `data` into the same shape as
the source definition.
- `jumpToMeasurement(viewportIndex, id)`: calls the listeners who have
- `jumpToMeasurement(viewportId, id)`: calls the listeners who have
subscribed to `JUMP_TO_MEASUREMENT`.
## Source / Mappers
@@ -48,7 +48,7 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
```js
const DEFAULT_STATE = {
viewportIndex: null,
viewportId: null,
message: undefined,
type: 'info', // "error" | "warning" | "info" | "success"
actions: undefined, // array of { type, text, value }
@@ -15,7 +15,7 @@ There are seven events that get publish in `ViewportGridService `:
| Event | Description |
| ----------------------------- | --------------------------------------------------|
| ACTIVE_VIEWPORT_INDEX_CHANGED | Fires the index of the active viewport is changed |
| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed |
| LAYOUT_CHANGED | Fires the layout is changed |
| GRID_STATE_CHANGED | Fires when the entire grid state is changed |
## Interface
@@ -25,9 +25,9 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| --------------------------------------------------------------------- | --------------------------------------------------- |
| `setActiveViewportIndex(index)` | Sets the active viewport index in the app |
| `setActiveViewportId(viewportId)` | Sets the active viewport Id in the app |
| `getState()` | Gets the states of the viewport (see below) |
| `setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id |
| `setDisplaySetsForViewport({ viewportId, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id |
| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. |
| `reset()` | Resets the default states |
| `getNumViewportPanes()` | Gets the number of visible viewport panes |
@@ -56,6 +56,6 @@ const DEFAULT_STATE = {
* }
*/
],
activeViewportIndex: 0,
activeViewportId: null,
};
```
@@ -25,7 +25,7 @@ Each `ViewportComponent` will receive the following props:
```html
<viewportComponent
viewportData="{viewportData}"
viewportIndex="{viewportIndex}"
viewportId="{viewportId}"
children="{[children]}"
/>
```
@@ -34,7 +34,7 @@ Each `ViewportComponent` will receive the following props:
| --------------- | --------------- | --------------------------------- |
| `children` | React.element[] | |
| `viewportData` | object | `viewportSpecificData` (probably) |
| `viewportIndex` | number | |
| `viewportId` | string | |
### `@ohif/viewer`
+1 -1
View File
@@ -10,7 +10,7 @@ const Footer = ({ actions, className, onSubmit, value }) => {
return (
<div className={classNames(flex, padding, className)}>
{actions.map((action, index) => {
{actions?.map((action, index) => {
const isFirst = index === 0;
const onClickHandler = event => onSubmit({ action, value, event });
@@ -73,9 +73,7 @@ const Notification = ({
<span className="ml-2 text-[13px] text-black">{message}</span>
</div>
<div className="flex flex-wrap gap-2 justify-end mt-2">
{actions.map((action, index) => {
const isFirst = index === 0;
{actions?.map((action, index) => {
return (
<Button
name={action.id}
@@ -1,18 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import LegacyViewportActionBar from '../LegacyViewportActionBar';
import Notification from '../Notification';
const Viewport = ({
interface StudyData {
label: string;
isTracked: boolean;
isLocked: boolean;
isRehydratable: boolean;
studyDate: string;
currentSeries: number;
seriesDescription: string;
modality: string;
patientInformation: {
patientName: string;
patientSex: string;
patientAge: string;
MRN: string;
thickness: string;
spacing: string;
scanner: string;
};
}
interface ViewportProps {
viewportId: string;
onArrowsClick: () => void;
studyData: StudyData;
children: React.ReactNode;
}
const Viewport: React.FC<ViewportProps> = ({
viewportId,
viewportIndex,
onArrowsClick,
studyData,
children,
}) => {
if (!viewportId) {
viewportId = `viewport-${viewportIndex}`;
}
return (
<div className="relative flex flex-col h-full">
<div className="absolute top-0 left-0 w-full">
@@ -58,30 +80,5 @@ const Viewport = ({
);
};
Viewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
onArrowsClick: PropTypes.func.isRequired,
studyData: PropTypes.shape({
label: PropTypes.string.isRequired,
isTracked: PropTypes.bool.isRequired,
/* Specific to SR Viewports only... */
isLocked: PropTypes.bool.isRequired,
isRehydratable: PropTypes.bool.isRequired,
studyDate: PropTypes.string.isRequired,
currentSeries: PropTypes.number.isRequired,
seriesDescription: PropTypes.string.isRequired,
modality: PropTypes.string.isRequired,
patientInformation: PropTypes.shape({
patientName: PropTypes.string.isRequired,
patientSex: PropTypes.string.isRequired,
patientAge: PropTypes.string.isRequired,
MRN: PropTypes.string.isRequired,
thickness: PropTypes.string.isRequired,
spacing: PropTypes.string.isRequired,
scanner: PropTypes.string.isRequired,
}),
}).isRequired,
children: PropTypes.node.isRequired,
};
export default Viewport;
export type { ViewportProps, StudyData };
@@ -1,2 +0,0 @@
import Viewport from './Viewport';
export default Viewport;
@@ -0,0 +1,5 @@
import Viewport from './Viewport';
import type { StudyData, ViewportProps } from './Viewport';
export default Viewport;
export type { StudyData, ViewportProps };
@@ -27,7 +27,6 @@ const ViewportActionBar = ({
getStatusComponent,
}: ViewportActionBarProps): JSX.Element => {
const { label, studyDate, seriesDescription, patientInformation } = studyData;
const {
patientName,
patientSex,
@@ -8,7 +8,6 @@ import React, {
import PropTypes from 'prop-types';
const DEFAULT_STATE = {
viewportIndex: null,
message: undefined,
type: 'info', // "error" | "warning" | "info" | "success"
actions: undefined, // array of { type, text, value }
@@ -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 =>