feat(grid): remove viewportIndex and only rely on viewportId (#3591)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in new issue
Block a user