feat: cornerstone3D stack and volume viewports (#2787)

* 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>
This commit is contained in:
authored and GitHub committed 2022-07-27 12:39:04 -04:00
1 parent 86dcaf6293
commit bd1c38c00c
668 files changed
+41669 -13410

No files matched your search

@@ -1,378 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF, { utils } from '@ohif/core';
import {
Notification,
ViewportActionBar,
useCine,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import { useTrackedMeasurements } from './../getContextModule';
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
import getTools from '../_shared/getTools';
const { formatDate } = utils;
// TODO -> Get this list from the list of tracked measurements.
// TODO -> We can now get a list of tool names from the measurement service.
// Use the toolnames to check which tools we have instead, using the
// Classes isn't really extensible unless we add the classes to the measurement
// Service definition, which feels wrong.
const {
ArrowAnnotateTool,
BidirectionalTool,
EllipticalRoiTool,
LengthTool,
} = cornerstoneTools;
const BaseAnnotationTool = cornerstoneTools.importInternal(
'base/BaseAnnotationTool'
);
const { StackManager } = OHIF.utils;
function TrackedCornerstoneViewport(props) {
const {
children,
dataSource,
displaySet,
viewportIndex,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { ToolBarService } = servicesManager.services;
const [trackedMeasurements] = useTrackedMeasurements();
const [
{ activeViewportIndex, viewports },
viewportGridService,
] = useViewportGrid();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
const [element, setElement] = useState(null);
const onElementEnabled = evt => {
const eventData = evt.detail;
const targetElement = eventData.element;
const tools = getTools();
const toolAlias = ToolBarService.state.primaryToolId;
// Activate appropriate tool bindings for element
setActiveAndPassiveToolsForElement(targetElement, tools);
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
mouseButtonMask: 1,
});
// Set dashed, based on tracking, for this viewport
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(
tool => tool.element === targetElement
);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
} else if (tool instanceof BaseAnnotationTool) {
const configuration = tool.configuration;
configuration.renderDashed = true;
tool.configuration = configuration;
}
});
// Update image after setting tool config
const enabledElement = cornerstone.getEnabledElement(targetElement);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
setElement(targetElement);
const OHIFCornerstoneEnabledElementEvent = new CustomEvent(
'ohif-cornerstone-enabled-element-event',
{
detail: {
context: 'ACTIVE_VIEWPORT::TRACKED',
enabledElement: targetElement,
viewportIndex,
},
}
);
document.dispatchEvent(OHIFCornerstoneEnabledElementEvent);
};
useEffect(() => {
if (!element) {
return;
}
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(tool => tool.element === element);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.image) {
cornerstone.updateImage(element);
}
}, [isTracked]);
// We have...
// StudyInstanceUid, DisplaySetInstanceUid
// Use displaySetInstanceUid --> SeriesInstanceUid
// Get meta for series, map to actionBar
// const displaySet = DisplaySetService.getDisplaySetByUID(
// dSet.displaySetInstanceUID
// );
// TODO: This display contains the meta for all instances.
// That can't be right...
// console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta);
// TODO: Share this logic so it isn't out of sync where we retrieve
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
);
const { trackedSeries } = trackedMeasurements.context;
const {
Modality,
SeriesDate,
SeriesDescription,
SeriesInstanceUID,
SeriesNumber,
} = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
SpacingBetweenSlices,
ManufacturerModelName,
} = displaySet.images[0];
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
const label = viewports.length > 1 ? _viewportLabels[viewportIndex] : '';
function switchMeasurement(direction) {
if (!element) {
// Element not yet enabled.
return;
}
const newTrackedMeasurementId = _getNextMeasurementId(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
);
if (!newTrackedMeasurementId) {
return;
}
setTrackedMeasurementId(newTrackedMeasurementId);
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const measurement = measurements.find(
m => m.id === newTrackedMeasurementId
);
setCornerstoneMeasurementActive(measurement);
MeasurementService.jumpToMeasurement(
viewportIndex,
newTrackedMeasurementId
);
}
const renderViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
return (
<Component
onElementEnabled={onElementEnabled}
element={element}
{...props}
></Component>
);
};
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onSeriesChange={direction => switchMeasurement(direction)}
studyData={{
label,
isTracked,
isLocked: false,
isRehydratable: false,
studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok?
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
seriesDescription: SeriesDescription,
modality: Modality,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness
? `${parseFloat(SliceThickness).toFixed(2)}mm`
: '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
}}
showNavArrows={!isCineEnabled}
showCine={isCineEnabled}
cineProps={{
isPlaying,
onClose: () => commandsManager.runCommand('toggleCine'),
onPlayPauseChange: isPlaying =>
cineService.setCine({ id: activeViewportIndex, isPlaying }),
onFrameRateChange: frameRate =>
cineService.setCine({ id: activeViewportIndex, frameRate }),
}}
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full overflow-hidden">
{renderViewport()}
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
id={viewportDialogState.id}
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
</div>
</>
);
}
TrackedCornerstoneViewport.propTypes = {
displaySet: PropTypes.object.isRequired,
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
TrackedCornerstoneViewport.defaultProps = {
customProps: {},
};
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
function _getNextMeasurementId(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
) {
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const { trackedSeries } = trackedMeasurements.context;
// Get the potentially trackable measurements for this series,
// The measurements to jump between are the same
// regardless if this series is tracked or not.
const filteredMeasurements = measurements.filter(m =>
trackedSeries.includes(m.referenceSeriesUID)
);
if (!filteredMeasurements.length) {
// No measurements on this series.
return;
}
const measurementCount = filteredMeasurements.length;
const ids = filteredMeasurements.map(fm => fm.id);
let measurementIndex = ids.findIndex(id => id === trackedMeasurementId);
if (measurementIndex === -1) {
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
measurementIndex = 0;
} else {
if (direction === 'left') {
measurementIndex--;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
}
} else if (direction === 'right') {
measurementIndex++;
if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
}
}
const newTrackedMeasurementId = ids[measurementIndex];
return newTrackedMeasurementId;
}
export default TrackedCornerstoneViewport;
@@ -0,0 +1,296 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import {
Notification,
ViewportActionBar,
useCine,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import { annotation } from '@cornerstonejs/tools';
import { useTrackedMeasurements } from './../getContextModule';
const { formatDate } = utils;
function TrackedCornerstoneViewport(props) {
const {
children,
displaySets,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const {
MeasurementService,
CornerstoneViewportService,
} = servicesManager.services;
// Todo: handling more than one displaySet on the same viewport
const displaySet = displaySets[0];
const [trackedMeasurements] = useTrackedMeasurements();
const [{ activeViewportIndex }] = useViewportGrid();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [viewportDialogState] = useViewportDialog();
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null);
const [element, setElement] = useState(null);
const { trackedSeries } = trackedMeasurements.context;
const viewportId = CornerstoneViewportService.getViewportId(viewportIndex);
const {
Modality,
SeriesDate,
SeriesDescription,
SeriesInstanceUID,
SeriesNumber,
} = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
SpacingBetweenSlices,
ManufacturerModelName,
} = displaySet.images[0];
useEffect(() => {
if (isTracked) {
annotation.config.style.setViewportToolStyles(viewportId, {
global: {
lineDash: '',
},
});
CornerstoneViewportService.getRenderingEngine().renderViewport(
viewportId
);
return;
}
annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, {
global: {
lineDash: '4,4',
},
});
CornerstoneViewportService.getRenderingEngine().renderViewport(viewportId);
return () => {
annotation.config.style.setViewportToolStyles(viewportId, {});
};
}, [isTracked]);
useEffect(() => {
if (!cines || !cines[viewportIndex]) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(element, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(element);
}
}, [cines, viewportIndex, cineService, element, displaySet]);
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
function switchMeasurement(direction) {
const newTrackedMeasurementUID = _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementUID,
trackedMeasurements
);
if (!newTrackedMeasurementUID) {
return;
}
setTrackedMeasurementUID(newTrackedMeasurementUID);
MeasurementService.jumpToMeasurement(
viewportIndex,
newTrackedMeasurementUID
);
}
const getCornerstoneViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
return <Component {...props} onElementEnabled={onElementEnabled} />;
};
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onSeriesChange={direction => switchMeasurement(direction)}
studyData={{
label: viewportLabel,
isTracked,
isLocked: false,
isRehydratable: false,
studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok?
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
seriesDescription: SeriesDescription,
modality: Modality,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness
? `${parseFloat(SliceThickness).toFixed(2)}mm`
: '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
}}
showNavArrows={!isCineEnabled}
showCine={isCineEnabled}
cineProps={{
isPlaying,
onClose: () => commandsManager.runCommand('toggleCine'),
onPlayPauseChange: isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
}),
onFrameRateChange: frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
}),
}}
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full overflow-hidden">
{getCornerstoneViewport()}
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
id={viewportDialogState.id}
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
</div>
</>
);
}
TrackedCornerstoneViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
TrackedCornerstoneViewport.defaultProps = {
customProps: {},
};
function _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
) {
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const { trackedSeries } = trackedMeasurements.context;
// Get the potentially trackable measurements for this series,
// The measurements to jump between are the same
// regardless if this series is tracked or not.
const filteredMeasurements = measurements.filter(m =>
trackedSeries.includes(m.referenceSeriesUID)
);
if (!filteredMeasurements.length) {
// No measurements on this series.
return;
}
const measurementCount = filteredMeasurements.length;
const uids = filteredMeasurements.map(fm => fm.uid);
let measurementIndex = uids.findIndex(uid => uid === trackedMeasurementId);
if (measurementIndex === -1) {
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
measurementIndex = 0;
} else {
if (direction === 'left') {
measurementIndex--;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
}
} else if (direction === 'right') {
measurementIndex++;
if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
}
}
const newTrackedMeasurementId = uids[measurementIndex];
return newTrackedMeasurementId;
}
export default TrackedCornerstoneViewport;
@@ -1,39 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const ViewportLoadingIndicator = ({ error }) => {
if (error) {
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
<p>{error.message}</p>
</div>
</>
);
}
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="absolute transparent w-full h-full flex items-center justify-center">
<p className="text-primary-light text-xl font-light">Loading...</p>
</div>
</>
);
};
ViewportLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;
@@ -1,95 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import classnames from 'classnames';
const ViewportOverlay = ({
imageId,
scale,
windowWidth,
windowCenter,
imageIndex,
stackSize,
activeTools,
}) => {
const topLeft = 'top-viewport left-viewport';
const topRight = 'top-viewport right-viewport-scrollbar';
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
const bottomLeft = 'bottom-viewport left-viewport';
const overlay = 'absolute pointer-events-none';
const isZoomActive = activeTools.includes('Zoom');
const isWwwcActive = activeTools.includes('Wwwc');
if (!imageId) {
return null;
}
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
return (
<div className="text-primary-light">
<div
data-cy={'viewport-overlay-top-left'}
className={classnames(overlay, topLeft)}
>
{isZoomActive && (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
)}
{isWwwcActive && (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">
{windowWidth.toFixed(0)}
</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, topRight)}
>
{stackSize > 1 && (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-light">
{`${instanceNumber} (${imageIndex}/${stackSize})`}
</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-bottom-right'}
className={classnames(overlay, bottomRight)}
></div>
<div
data-cy={'viewport-overlay-bottom-left'}
className={classnames(overlay, bottomLeft)}
></div>
</div>
);
};
ViewportOverlay.propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.number.isRequired,
windowCenter: PropTypes.number.isRequired,
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
activeTools: PropTypes.arrayOf(PropTypes.string),
};
ViewportOverlay.defaultProps = {
activeTools: [],
};
export default ViewportOverlay;