* feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock * feat: overlay component (#21) * fix default displayset options * add viewport overlay * apply review comments * feat: loading and orientation indicators (#22) * add loading indicator * add orientation marker initial work * apply review comments * fix: orientation markers (#23) * finished the orientation markers * fix various broken cypress tests * apply review comments * update yarn lock * feat: re-working measurement tracking mode with cornerstone3d (#2805) * feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: Add Measurement tracking mode with cs3D (#2789) * feat: first render for cornerstone3d tracked viewport * make tool active work * wip for SR extension * renamed dicom sr to cornerstone dicom sr * remove cornerstone from dicom pdf and video * move dicom sr logic to sr extension * feat: Add hydration for the length tool * fix SR display tool for length using cs3d * fix default config * fix: various bugs with sr viewport and tracking * fix promptying to continue tracking for when SR is created * feat: add keep trackign of unique identifiers * fix hydration for same imageIds * feat: Add SR toolGroup creation on modeEnter * feat: remove the need for separate mapper for SR hydration * add SR display for ellipse * handle hydration of elliptical ROI tool * remove cornerstone extension * add arrow mapping * feat: Add ArrowAnnotate SR display and hydration * apply review comments * apply review comments * move viewport labels to the viewportData * apply review comments * fix: integration cypress tests with cornerstone3D and add CINE tool (#2795) * fix: integration cypress tests with cornerstone3D * revert to addOrUpdate as it makes more sense * fix local drag and drop * fix tests * move dicomLoaderService to cornerstone extension * fix various import bugs * fix bug for local PT series * fix various unit tests * bump cs3d versions * add angle and magnify tool * bump dependencies to avoid broken peerDeps * feat: add initial work for capture using cs3d * feat: show annotations on the image capture * feat: add svg layer export * feat: Add CINE Tool * feat: remove unnecessary viewport rendering for cine state changes Co-authored-by: Erik Ziegler <erik.sweed@gmail.com> * docs: modify and improve documentation (#2800) * cleanup docs versionings * feat: Add all version explanations * version docs for 3.0 * wip for changing docs * wip for updated docs * add utility module documentation * fix demo with nohoisting of history * add slides and video to resources * apply review comments * fix: drag and drop SR into SR viewport (#2803) * update yarn lock Co-authored-by: Erik <erik.sweed@gmail.com> * update pathnames to match v3-stable * update the e2e pathname * fix: various bugs with regard to tracking workflow (#2811) * fix: various issues with measurement panel * fix: update default tool style for annotations * fix: annotatoin label getting removed * feat: Add backward compatibility for SR hydration with legacy cornerstone * fix: cursors and ellipse ROI max style * fix: ArrowAnnotate SRDisplay * apply review comments * bump package versions * fix: bugin rehydration of SR * fix: e2e tests * fix active viewport thickness and arrowTool ui * add readme for measurement tracking * use uploaded image for readme * add back images * try to fix e2e test * fix: window level presets hotkeys * Update README.md * update yarn lock * feat: volume api and TMTV mode (#2817) * feat: volumeAPI and TMTV mode * feat: use cs3d cache service to obtain viewportData * wip for volume api * wip: fusion viewport * feat: add blend mode option * wip for image scrollbar * fix drag and drop thumbnail * wip for image scrollbar for voluems * fix: element mismatch bug for scroll * feat: Add image scrollbar to volumes * feat: add syncGroups to volume api * feat: add tmtv mode initial setup * feat: add initial image options for the stack viewports * feat: Add custom load strategy for volume viewports via HP * apply review comments fix: Jump presets cs3d (#2812) * feat: Add JumpPreset to OHIF for Cornerstone3D * fix: accessing viewport service from servicesManager feat: volume API and TMTV mode (#2814) * fix: do not display overlays on mip viewports * feat: add optional disableCommands for toggle buttons * fix: toggleCrossharis * feat: config the crosshairs * feat: add PetSUV Panel for changing metadata * feat: initial work for the rectangleROIThreshold panel * wip: measurement service * roi threshold working * feat: Add displayText to segmentations * feat: add remove segmentation * feat: add csv export * feat: add RT export for annotations in tmtv mode * fix: fusion to use pt in tmtv mode and measuremet mappings * fix: various bugs * apply review comments * apply review comments * feat: add fusion color maps * add readme to tmtv mode * Update README.md * fix: try to fix build * fix unit tests * Update README.md * feat: add about to the panel * fix: changing strategy in roi panel * apply review comments * update package versions * wip for stackPrefetch * fix: cornerstone3d hydration and renaming (#2818) * update readme * renamed cornerstone extension * wip for renaming cornerstone3D variables * wip for renaming cornerstone3D variables * wip for fixing bugs for SR viewport * fix: jumpToMeasurement and initial label after hydration * fix: fileName capitalization * feat: use the new prefetch stack in the cs3d (#2820) * feat: use the new prefetch stack in the cs3d * use viewport scroll api for stack viewport * fix cine stop when scrollbar changes * feat: use new prefetch events * fix loading state to not show repeatedly * fix: various bugs for tmtv mode thresholding and new icons (#2823) * feat: make tmtv mode available in worklist * feat: add new icons for tmtv mode * feat: add fusion color icon * fix: parallel scale calculation * fix: bump Cornerstone to get large image support working * fix: Fix issues with CPU viewport flipping, including config files * fix: bump cornerstone to fix magnify tool * fix: Bump Cornerstone version to fix resetCamera issue in StackViewport * ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys * fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors * feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838) Also works with the compile time add that the existing cli uses, so that both build types work. Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes. * bump: dependency versions to fix hydration bugs (#2848) * bump: dcmjs version to fix hydration bugs * try to fix tests * bump dependency versions Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
348 lines
9.2 KiB
TypeScript
348 lines
9.2 KiB
TypeScript
import React, {
|
|
useState,
|
|
createContext,
|
|
useContext,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
} from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
|
import Draggable from 'react-draggable';
|
|
import classNames from 'classnames';
|
|
|
|
/*
|
|
* This is a workaround to import things from ohif/core as docz does
|
|
* not allow us to access window element and @ohif/core does use it once
|
|
* we import to instanciate cornerstone
|
|
*/
|
|
import guid from './../../../core/src/utils/guid';
|
|
|
|
import './DialogProvider.css';
|
|
|
|
const DialogContext = createContext(null);
|
|
|
|
export const useDialog = () => useContext(DialogContext);
|
|
|
|
const DialogProvider = ({ children, service }) => {
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [dialogs, setDialogs] = useState([]);
|
|
const [lastDialogId, setLastDialogId] = useState(null);
|
|
const [lastDialogPosition, setLastDialogPosition] = useState(null);
|
|
const [centerPositions, setCenterPositions] = useState([]);
|
|
|
|
useEffect(() => {
|
|
setCenterPositions(
|
|
dialogs.map(dialog => ({
|
|
id: dialog.id,
|
|
...getCenterPosition(dialog.id),
|
|
}))
|
|
);
|
|
}, [dialogs]);
|
|
|
|
const getCenterPosition = id => {
|
|
const root = document.querySelector('#root');
|
|
const centerX = root.offsetLeft + root.offsetWidth / 2;
|
|
const centerY = root.offsetTop + root.offsetHeight / 2;
|
|
const item = document.querySelector(`#draggableItem-${id}`);
|
|
const itemBounds = item.getBoundingClientRect();
|
|
return {
|
|
x: centerX - itemBounds.width / 2,
|
|
y: centerY - itemBounds.height / 2,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* UI Dialog
|
|
*
|
|
* @typedef {Object} DialogProps
|
|
* @property {string} id The dialog id.
|
|
* @property {DialogContent} content The dialog content.
|
|
* @property {Object} contentProps The dialog content props.
|
|
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
|
* @property {boolean} showOverlay Controls dialog overlay.
|
|
* @property {boolean} centralize Center the dialog on the screen.
|
|
* @property {boolean} preservePosition Use last position instead of default.
|
|
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
|
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
|
* @property {Function} onStop Called when dragging stops.
|
|
* @property {Function} onDrag Called while dragging.
|
|
*/
|
|
|
|
/**
|
|
* Creates a new dialog and return its id.
|
|
*
|
|
* @param {DialogProps} props The dialog props.
|
|
* @returns The new dialog id.
|
|
*/
|
|
const create = useCallback(props => {
|
|
const { id } = props;
|
|
|
|
let dialogId = id;
|
|
if (!dialogId) {
|
|
dialogId = guid();
|
|
}
|
|
|
|
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
|
setLastDialogId(dialogId);
|
|
|
|
return dialogId;
|
|
}, []);
|
|
|
|
/**
|
|
* Dismisses the dialog with a given id.
|
|
*
|
|
* @param {Object} props -
|
|
* @property {string} props.id The dialog id.
|
|
* @returns void
|
|
*/
|
|
const dismiss = useCallback(
|
|
({ id }) =>
|
|
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
|
|
[]
|
|
);
|
|
|
|
/**
|
|
* Dismisses all dialogs.
|
|
*
|
|
* @returns void
|
|
*/
|
|
const dismissAll = () => {
|
|
setDialogs([]);
|
|
};
|
|
|
|
/**
|
|
* Indicate if there are no dialogs present.
|
|
*
|
|
* @returns True if no dialogs are present.
|
|
*/
|
|
const isEmpty = () => dialogs && dialogs.length < 1;
|
|
|
|
/**
|
|
* Moves the dialog to the foreground if clicked.
|
|
*
|
|
* @param {string} id The dialog id.
|
|
* @returns void
|
|
*/
|
|
const _bringToFront = useCallback(id => {
|
|
setDialogs(dialogs => {
|
|
const topDialog = dialogs.find(dialog => dialog.id === id);
|
|
return topDialog
|
|
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
|
: dialogs;
|
|
});
|
|
}, []);
|
|
|
|
/**
|
|
* Sets the implementation of a dialog service that can be used by extensions.
|
|
*
|
|
* @returns void
|
|
*/
|
|
useEffect(() => {
|
|
if (service) {
|
|
service.setServiceImplementation({ create, dismiss, dismissAll });
|
|
}
|
|
}, [create, dismiss, service]);
|
|
|
|
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
|
|
|
|
const renderDialogs = () =>
|
|
dialogs.map(dialog => {
|
|
const {
|
|
id,
|
|
content: DialogContent,
|
|
contentProps,
|
|
defaultPosition,
|
|
centralize = false,
|
|
preservePosition = true,
|
|
isDraggable = true,
|
|
onStart,
|
|
onStop,
|
|
onDrag,
|
|
onClickOutside,
|
|
showOverlay,
|
|
} = dialog;
|
|
|
|
let position =
|
|
(preservePosition && lastDialogPosition) || defaultPosition;
|
|
if (centralize) {
|
|
position = centerPositions.find(position => position.id === id);
|
|
}
|
|
|
|
const dragableItem = () => (
|
|
<Draggable
|
|
key={id}
|
|
disabled={!isDraggable}
|
|
position={position}
|
|
defaultPosition={position}
|
|
bounds="parent"
|
|
onStart={event => {
|
|
const e = event || (typeof window !== 'undefined' && window.event);
|
|
const target = e.target || e.srcElement;
|
|
const BLACKLIST = [
|
|
'SVG',
|
|
'BUTTON',
|
|
'PATH',
|
|
'INPUT',
|
|
'SPAN',
|
|
'LABEL',
|
|
];
|
|
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
|
|
return false;
|
|
}
|
|
|
|
if (validCallback(onStart)) {
|
|
return onStart(event);
|
|
}
|
|
}}
|
|
onStop={event => {
|
|
setIsDragging(false);
|
|
|
|
if (validCallback(onStop)) {
|
|
return onStop(event);
|
|
}
|
|
}}
|
|
onDrag={event => {
|
|
setIsDragging(true);
|
|
_bringToFront(id);
|
|
_updateLastDialogPosition(id);
|
|
|
|
if (validCallback(onDrag)) {
|
|
return onDrag(event);
|
|
}
|
|
}}
|
|
>
|
|
<div
|
|
id={`draggableItem-${id}`}
|
|
className={classNames(
|
|
'DraggableItem',
|
|
isDragging && 'dragging',
|
|
isDraggable && 'draggable'
|
|
)}
|
|
style={{ zIndex: '999', position: 'absolute' }}
|
|
onClick={() => _bringToFront(id)}
|
|
>
|
|
<DialogContent {...dialog} {...contentProps} />
|
|
</div>
|
|
</Draggable>
|
|
);
|
|
|
|
const withOverlay = component => {
|
|
const background = 'bg-black bg-opacity-50';
|
|
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
|
|
return (
|
|
<div className={classNames(overlay, background)} key={id}>
|
|
{component}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
let result = dragableItem();
|
|
|
|
if (showOverlay) {
|
|
result = withOverlay(result);
|
|
}
|
|
|
|
if (typeof onClickOutside === 'function') {
|
|
result = (
|
|
<OutsideAlerter key={id} onClickOutside={onClickOutside}>
|
|
{result}
|
|
</OutsideAlerter>
|
|
);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
/**
|
|
* Update the last dialog position to be used as the new default position.
|
|
*
|
|
* @returns void
|
|
*/
|
|
const _updateLastDialogPosition = dialogId => {
|
|
const draggableItemBounds = document
|
|
.querySelector(`#draggableItem-${dialogId}`)
|
|
.getBoundingClientRect();
|
|
setLastDialogPosition({
|
|
x: draggableItemBounds.x,
|
|
y: draggableItemBounds.y,
|
|
});
|
|
};
|
|
|
|
const onKeyDownHandler = event => {
|
|
if (event.key === 'Escape') {
|
|
dismissAll();
|
|
}
|
|
};
|
|
|
|
const validCallback = callback => callback && typeof callback === 'function';
|
|
|
|
return (
|
|
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
|
{!isEmpty() && (
|
|
<div className="w-full h-full absolute" onKeyDown={onKeyDownHandler}>
|
|
{renderDialogs()}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</DialogContext.Provider>
|
|
);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* High Order Component to use the dialog methods through a Class Component
|
|
*
|
|
*/
|
|
export const withDialog = Component => {
|
|
return function WrappedComponent(props) {
|
|
const { create, dismiss, dismissAll, isEmpty } = useDialog();
|
|
return (
|
|
<Component {...props} dialog={{ create, dismiss, dismissAll, isEmpty }} />
|
|
);
|
|
};
|
|
};
|
|
|
|
DialogProvider.defaultProps = {
|
|
service: null,
|
|
};
|
|
|
|
DialogProvider.propTypes = {
|
|
children: PropTypes.oneOfType([
|
|
PropTypes.arrayOf(PropTypes.node),
|
|
PropTypes.node,
|
|
PropTypes.func,
|
|
]).isRequired,
|
|
service: PropTypes.shape({
|
|
setServiceImplementation: PropTypes.func,
|
|
}),
|
|
};
|
|
|
|
export default DialogProvider;
|
|
|
|
function OutsideAlerter(props) {
|
|
const wrapperRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
/**
|
|
* Alert if clicked on outside of element
|
|
*/
|
|
function handleInteractionOutside(event) {
|
|
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
props.onClickOutside();
|
|
}
|
|
}
|
|
|
|
// Bind the event listener
|
|
document.addEventListener('mousedown', handleInteractionOutside);
|
|
document.addEventListener('touchstart', handleInteractionOutside);
|
|
return () => {
|
|
// Unbind the event listener on clean up
|
|
document.removeEventListener('mousedown', handleInteractionOutside);
|
|
document.removeEventListener('touchstart', handleInteractionOutside);
|
|
};
|
|
}, [wrapperRef]);
|
|
|
|
return <div ref={wrapperRef}>{props.children}</div>;
|
|
}
|