empty viewport, prevent duplicate series meta from add, protect broadcast events when no data has changed

This commit is contained in:
dannyrb 2020-05-23 22:15:21 -04:00
parent 692f4c1ca5
commit c9a4ab13ef
13 changed files with 198 additions and 146 deletions

View File

@ -51,7 +51,7 @@ function PanelStudyBrowser({
// ~~ Initial Thumbnails
useEffect(() => {
const currentDisplaySets = DisplaySetService.activeDisplaySets || [];
const currentDisplaySets = DisplaySetService.activeDisplaySets;
currentDisplaySets.forEach(async dSet => {
const newImageSrcEntry = {};
const displaySet = DisplaySetService.getDisplaySetByUID(
@ -71,7 +71,7 @@ function PanelStudyBrowser({
// ~~ displaySets
useEffect(() => {
// TODO: Are we sure `activeDisplaySets` will always be accurate?
const currentDisplaySets = DisplaySetService.activeDisplaySets || [];
const currentDisplaySets = DisplaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
thumbnailImageSrcMap
@ -164,6 +164,7 @@ function PanelStudyBrowser({
PanelStudyBrowser.propTypes = {
DisplaySetService: PropTypes.shape({
EVENTS: PropTypes.object.isRequired,
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
getDisplaySetByUID: PropTypes.func.isRequired,
hasDisplaySetsForStudy: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,

View File

@ -72,11 +72,16 @@ const BaseImplementation = {
study = _model.studies[_model.studies.length - 1];
}
study.addSeries(instances);
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
StudyInstanceUID,
SeriesInstanceUID,
});
// TODO: Worth identifying why this is being called many times with series
// that are already "added"?
const didAddSeries = study.addSeries(instances);
if (didAddSeries) {
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
StudyInstanceUID,
SeriesInstanceUID,
});
}
},
addStudy(study) {
const { StudyInstanceUID } = study;

View File

@ -4,9 +4,24 @@ function createStudyMetadata(StudyInstanceUID) {
return {
StudyInstanceUID,
series: [],
/**
*
* @param {object[]} instances
* @param {string} instances[].SeriesInstanceUID
* @returns {bool} true if series were added; false if series already exist
*/
addSeries: function(instances) {
const series = createSeriesMetadata(instances);
this.series.push(series);
const { SeriesInstanceUID } = instances[0];
const seriesExists = this.series.some(
s => s.SeriesInstanceUID === SeriesInstanceUID
);
if (!seriesExists) {
const series = createSeriesMetadata(instances);
this.series.push(series);
}
return !seriesExists;
},
};
}

View File

@ -6,6 +6,7 @@ const displaySetCache = [];
export default class DisplaySetService {
constructor() {
this.displaySets = {};
this.activeDisplaySets = [];
this.listeners = {};
this.EVENTS = EVENTS;
@ -77,9 +78,10 @@ export default class DisplaySetService {
displaySetsAdded = displaySets;
}
// TODO: This is tricky. How do we know we're not resetting to the same/existing DSs?
// TODO: This is likely run anytime we touch DicomMetadataStore. How do we prevent uneccessary broadcasts?
if (displaySetsAdded && displaySetsAdded.length) {
this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, displaySetsAdded);
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
}
};

View File

@ -13,7 +13,7 @@ export default {
};
/**
* Subscribe to measurement updates.
* Subscribe to updates.
*
* @param {string} eventName The name of the event
* @param {Function} callback Events callback
@ -24,7 +24,7 @@ function subscribe(eventName, callback) {
const listenerId = guid();
const subscription = { id: listenerId, callback };
console.info(`displaySetService: Subscribing to '${eventName}'.`);
console.info(`Subscribing to '${eventName}'.`);
if (Array.isArray(this.listeners[eventName])) {
this.listeners[eventName].push(subscription);
} else {
@ -60,7 +60,7 @@ function _unsubscribe(eventName, listenerId) {
}
/**
* Check if a given displaySetService event is valid.
* Check if a given event is valid.
*
* @param {string} eventName The name of the event
* @return {boolean} Event name validation
@ -70,7 +70,7 @@ function _isValidEvent(eventName) {
}
/**
* Broadcasts displaySetService changes.
* Broadcasts changes.
*
* @param {string} eventName - The event name
* @param {func} callbackProps - Properties to pass callback

View File

@ -14,6 +14,9 @@ export {
ImageViewerContext,
ImageViewerProvider,
useImageViewer,
ViewportGridContext,
ViewportGridProvider,
useViewportGrid,
} from './src/contextProviders';
/** COMPONENTS */

View File

@ -0,0 +1,20 @@
import React, { createContext, useContext, useReducer } from 'react';
// export const VIEWPORT_GRID_DEFAULT_VALUE = {
// numCols: 1,
// numRows: 1,
// activeViewportIndex: 0,
// viewports: [],
// };
export const ViewportGridContext = createContext();
export function ViewportGridProvider({ reducer, initialState, children }) {
return (
<ViewportGridContext.Provider value={useReducer(reducer, initialState)}>
{children}
</ViewportGridContext.Provider>
);
}
export const useViewportGrid = () => useContext(ViewportGridContext);

View File

@ -16,4 +16,10 @@ export {
useImageViewer,
} from './ImageViewerProvider';
export {
ViewportGridContext,
ViewportGridProvider,
useViewportGrid,
} from './ViewportGridProvider';
export { default as DragAndDropProvider } from './DragAndDropProvider';

View File

@ -6,7 +6,7 @@ import { ThemeWrapper } from '@ohif/ui';
// Viewer Project
// TODO: Should this influence study list?
import { appConfigContext } from '@state/appConfig.context';
import { useAppConfig } from './hooks/useAppConfig';
import { useAppConfig } from '@hooks/useAppConfig';
import createRoutes from './routes';
import appInit from './appInit.js';

View File

@ -0,0 +1,11 @@
import React from 'react';
function EmptyViewport() {
return (
<div>
<p>Please drag a stack here to view images.</p>
</div>
);
}
export default EmptyViewport;

View File

@ -1,19 +1,17 @@
/**
* CSS Grid Reference: http://grid.malven.co/
*/
import React, { useState, useCallback, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { ViewportGrid, ViewportPane } from '@ohif/ui';
// import DefaultViewport from './DefaultViewport.js';
// import EmptyViewport from './EmptyViewport.js';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
import EmptyViewport from './EmptyViewport';
function ViewerViewportGrid(props) {
const {
activeViewportIndex,
servicesManager,
viewportComponents,
dataSource,
} = props;
const { servicesManager, viewportComponents, dataSource } = props;
const [
{ numCols, numRows, activeViewportIndex, viewports },
dispatch,
] = useViewportGrid();
// TODO -> Need some way of selecting which displaySets hit the viewports.
const { DisplaySetService } = servicesManager.services;
@ -23,8 +21,8 @@ function ViewerViewportGrid(props) {
let displaySetInstanceUID;
// Fallback
if(!displaySets || !displaySets.length) {
const displaySet = DisplaySetService.activeDisplaySets[0]
if (!displaySets || !displaySets.length) {
const displaySet = DisplaySetService.activeDisplaySets[0];
displaySetInstanceUID = displaySet.displaySetInstanceUID;
} else {
const displaySet = displaySets[0];
@ -43,86 +41,97 @@ function ViewerViewportGrid(props) {
};
};
// From ViewportGridService and/or ContextProvider
const [viewportGrid, setViewportGrid] = useState({
numCols: 1,
numRows: 1,
viewports: []
});
// TODO:
// Hmm... Should a "displaySet" being added update the viewport based on HP?
// I guess it might.
// This is where you would likely "fill" emptyViewports if none had content
// Or to recheck best placement/priority based on all activeDisplaySets
// useEffect(() => {
// const { unsubscribe } = DisplaySetService.subscribe(
// DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
// displaySets => {
// displaySets.forEach(ds => console.log(`VPG:ADD::${ds.StudyInstanceUID}`));
// const hp = HangingProtocolService(displaySets);
// viewportGrid.setViewportGrid(hp);
// },
// );
useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
displaySets => {
setViewportGrid(HangingProtocolService(displaySets));
},
);
return unsubscribe;
}, []);
// return unsubscribe;
// }, []);
// TODO: either need hover to change "active viewport"
// so we can use it as our target for setting the displaySet,
// or the dropHandler needs to know which viewport was dropped on
// in event data
const onDropHandler = ({displaySetInstanceUID}) => {
const droppedDisplaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID);
const updatedViewportGridState = HangingProtocolService([droppedDisplaySet]);
const onDropHandler = ({ displaySetInstanceUID }) => {
const droppedDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const updatedViewportGridState = HangingProtocolService([
droppedDisplaySet,
]);
console.warn('DROPPED: ', displaySetInstanceUID, droppedDisplaySet, updatedViewportGridState);
console.warn(
'DROPPED: ',
displaySetInstanceUID,
droppedDisplaySet,
updatedViewportGridState
);
// This is not updating the displayed DisplaySet
setViewportGrid(updatedViewportGridState);
}
dispatch({ type: 'action-name', payload: updatedViewportGridState });
};
// viewportData --> displaySets
const getViewportPanes = () =>
viewportGrid.viewports.map((viewport, viewportIndex) => {
const displaySetInstanceUID = viewport.displaySetInstanceUID;
if (!displaySetInstanceUID) {
return null;
}
const getViewportPanes = () => {
const viewportPanes = [];
const numViewportPanes = numCols * numRows;
const displaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID);
for (let i = 0; i < numViewportPanes; i++) {
const viewportIndex = i;
const paneMeta = viewports[i];
const isEmpty = !paneMeta || !paneMeta.displaySetInstanceUID;
// TODO: Better Empty Viewport
if (!displaySet) {
return (
if (isEmpty) {
viewportPanes[i] = (
<ViewportPane
key={viewportIndex}
className="m-1"
// Pass in as prop?
acceptDropsFor="displayset"
onDrop={onDropHandler}
isActive={activeViewportIndex === viewportIndex}
/>
>
<EmptyViewport />
</ViewportPane>
);
} else {
const displaySet = DisplaySetService.getDisplaySetByUID(
paneMeta.displaySetInstanceUID
);
const ViewportComponent = _getViewportComponent(
displaySet,
viewportComponents
);
viewportPanes[i] = (
<ViewportPane
key={viewportIndex}
className="m-1"
acceptDropsFor="displayset"
onDrop={onDropHandler}
isActive={activeViewportIndex === viewportIndex}
>
<ViewportComponent
displaySet={displaySet}
viewportIndex={viewportIndex}
dataSource={dataSource}
/>
</ViewportPane>
);
}
}
// TODO -> Need way for other viewport e.g. vtk to be used.
const ViewportComponent = _getViewportComponent(
displaySet,
viewportComponents
);
return (
<ViewportPane
key={viewportIndex}
className="m-1"
acceptDropsFor="displayset"
onDrop={onDropHandler}
isActive={activeViewportIndex === viewportIndex}
>
<ViewportComponent
displaySet={displaySet}
viewportIndex={viewportIndex}
dataSource={dataSource}
/>
</ViewportPane>
);
});
return viewportPanes;
};
// const ViewportPanes = React.useMemo(getViewportPanes, [
// viewportComp'onents,
@ -131,7 +140,7 @@ function ViewerViewportGrid(props) {
// ]);
return (
<ViewportGrid numRows={viewportGrid.numRows} numCols={viewportGrid.numCols}>
<ViewportGrid numRows={numRows} numCols={numCols}>
{/* {ViewportPanes} */}
{getViewportPanes()}
</ViewportGrid>
@ -139,19 +148,11 @@ function ViewerViewportGrid(props) {
}
ViewerViewportGrid.propTypes = {
// viewports: PropTypes.array.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
viewportComponents: PropTypes.array.isRequired,
// numRows: PropTypes.number.isRequired,
// numColumns: PropTypes.number.isRequired,
};
ViewerViewportGrid.defaultProps = {
// numRows: 1,
// numColumns: 1,
viewportData: [],
viewportComponents: [],
activeViewportIndex: 0,
};
function _getViewportComponent(displaySet, viewportComponents) {

View File

@ -3,7 +3,11 @@ import { useParams } from 'react-router';
import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore, ToolBarManager } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import {
DragAndDropProvider,
ImageViewerProvider,
ViewportGridProvider,
} from '@ohif/ui';
//
import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
@ -119,23 +123,44 @@ export default function ModeRoute({
console.log(state, action);
};
const viewportGridReducer = (state, action) => {
console.log(state, action);
switch (action.type) {
case 'DO_TODO':
return state;
default:
return action.payload;
}
};
return (
<ImageViewerProvider
initialState={{ StudyInstanceUIDs: StudyInstanceUIDsAsArray }}
reducer={reducer}
>
<CombinedContextProvider>
{/* TODO: extensionManager is already provided to the extension module.
* Use it from there instead of passing as a prop here.
*/}
<DragAndDropProvider>
<LayoutComponent
{...layoutTemplateData.props}
StudyInstanceUIDs={StudyInstanceUIDs}
ViewportGridComp={ViewportGridWithDataSource}
/>
</DragAndDropProvider>
</CombinedContextProvider>
<ViewportGridProvider
initialState={{
numRows: 1,
numCols: 1,
viewports: [],
activeViewportIndex: 0,
}}
reducer={viewportGridReducer}
>
<CombinedContextProvider>
{/* TODO: extensionManager is already provided to the extension module.
* Use it from there instead of passing as a prop here.
*/}
<DragAndDropProvider>
<LayoutComponent
{...layoutTemplateData.props}
StudyInstanceUIDs={StudyInstanceUIDs}
ViewportGridComp={ViewportGridWithDataSource}
/>
</DragAndDropProvider>
</CombinedContextProvider>
</ViewportGridProvider>
</ImageViewerProvider>
);
}

View File

@ -1,37 +0,0 @@
import React, { useState, createContext, useContext } from 'react';
import PropTypes from 'prop-types';
const ViewportGridContext = createContext(null);
export const useViewportGrid = () => useContext(ViewportGridContext);
export default function ViewportGridProvider({ children }) {
const [viewportGrid, setViewportGrid] = useState({
rows: 1,
columns: 1,
viewports: [],
});
const get = () => {
return viewportGrid;
};
const set = ({ rows, columns, viewports }) => {
setViewportGrid({ rows, columns, viewports });
};
return (
<ViewportGridProvider.Provider value={{ get, set }}>
{/* {children} */}
</ViewportGridProvider.Provider>
);
}
ViewportGridProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export default ViewportGridProvider;