* 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>
365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
|
|
import { utils } from '@ohif/core';
|
|
|
|
const { formatDate } = utils;
|
|
|
|
/**
|
|
*
|
|
* @param {*} param0
|
|
*/
|
|
function PanelStudyBrowser({
|
|
DisplaySetService,
|
|
getImageSrc,
|
|
getStudiesForPatientByStudyInstanceUID,
|
|
requestDisplaySetCreationForStudy,
|
|
dataSource,
|
|
}) {
|
|
// Normally you nest the components so the tree isn't so deep, and the data
|
|
// doesn't have to have such an intense shape. This works well enough for now.
|
|
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
|
const { StudyInstanceUIDs } = useImageViewer();
|
|
const [
|
|
{ activeViewportIndex, viewports },
|
|
viewportGridService,
|
|
] = useViewportGrid();
|
|
const [activeTabName, setActiveTabName] = useState('primary');
|
|
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
|
...StudyInstanceUIDs,
|
|
]);
|
|
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
|
const [displaySets, setDisplaySets] = useState([]);
|
|
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
|
const isMounted = useRef(true);
|
|
|
|
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
|
viewportGridService.setDisplaySetsForViewport({
|
|
viewportIndex: activeViewportIndex,
|
|
displaySetInstanceUIDs: [displaySetInstanceUID],
|
|
});
|
|
};
|
|
|
|
// ~~ studyDisplayList
|
|
useEffect(() => {
|
|
// Fetch all studies for the patient in each primary study
|
|
async function fetchStudiesForPatient(StudyInstanceUID) {
|
|
const qidoStudiesForPatient =
|
|
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
|
|
|
|
// TODO: This should be "naturalized DICOM JSON" studies
|
|
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
|
|
|
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
|
return {
|
|
studyInstanceUid: qidoStudy.StudyInstanceUID,
|
|
date: formatDate(qidoStudy.StudyDate),
|
|
description: qidoStudy.StudyDescription,
|
|
modalities: qidoStudy.ModalitiesInStudy,
|
|
numInstances: qidoStudy.NumInstances,
|
|
// displaySets: []
|
|
};
|
|
});
|
|
if (isMounted.current) {
|
|
setStudyDisplayList(actuallyMappedStudies);
|
|
}
|
|
}
|
|
|
|
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
|
|
|
|
// // ~~ Initial Thumbnails
|
|
useEffect(() => {
|
|
const currentDisplaySets = DisplaySetService.activeDisplaySets;
|
|
currentDisplaySets.forEach(async dSet => {
|
|
const newImageSrcEntry = {};
|
|
const displaySet = DisplaySetService.getDisplaySetByUID(
|
|
dSet.displaySetInstanceUID
|
|
);
|
|
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
|
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
|
|
|
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
|
if (imageId) {
|
|
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
|
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
|
imageId
|
|
);
|
|
if (isMounted.current) {
|
|
setThumbnailImageSrcMap(prevState => {
|
|
return { ...prevState, ...newImageSrcEntry };
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return () => {
|
|
isMounted.current = false;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// ~~ displaySets
|
|
useEffect(() => {
|
|
// TODO: Are we sure `activeDisplaySets` will always be accurate?
|
|
const currentDisplaySets = DisplaySetService.activeDisplaySets;
|
|
const mappedDisplaySets = _mapDisplaySets(
|
|
currentDisplaySets,
|
|
thumbnailImageSrcMap
|
|
);
|
|
|
|
setDisplaySets(mappedDisplaySets);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [thumbnailImageSrcMap]);
|
|
|
|
// ~~ subscriptions --> displaySets
|
|
useEffect(() => {
|
|
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
|
|
const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe(
|
|
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
|
data => {
|
|
const { displaySetsAdded } = data;
|
|
displaySetsAdded.forEach(async dSet => {
|
|
const newImageSrcEntry = {};
|
|
const displaySet = DisplaySetService.getDisplaySetByUID(
|
|
dSet.displaySetInstanceUID
|
|
);
|
|
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
|
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
|
|
|
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
|
if (imageId) {
|
|
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
|
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
|
imageId,
|
|
dSet.initialViewport
|
|
);
|
|
if (isMounted.current) {
|
|
setThumbnailImageSrcMap(prevState => {
|
|
return { ...prevState, ...newImageSrcEntry };
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
// TODO: Will this always hold _all_ the displaySets we care about?
|
|
// DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
|
|
const SubscriptionDisplaySetsChanged = DisplaySetService.subscribe(
|
|
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
|
changedDisplaySets => {
|
|
const mappedDisplaySets = _mapDisplaySets(
|
|
changedDisplaySets,
|
|
thumbnailImageSrcMap
|
|
);
|
|
setDisplaySets(mappedDisplaySets);
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
SubscriptionDisplaySetsAdded.unsubscribe();
|
|
SubscriptionDisplaySetsChanged.unsubscribe();
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const tabs = _createStudyBrowserTabs(
|
|
StudyInstanceUIDs,
|
|
studyDisplayList,
|
|
displaySets
|
|
);
|
|
|
|
// TODO: Should not fire this on "close"
|
|
function _handleStudyClick(StudyInstanceUID) {
|
|
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(
|
|
StudyInstanceUID
|
|
);
|
|
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
|
? // eslint-disable-next-line prettier/prettier
|
|
[
|
|
...expandedStudyInstanceUIDs.filter(
|
|
stdyUid => stdyUid !== StudyInstanceUID
|
|
),
|
|
]
|
|
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
|
|
|
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
|
|
|
if (!shouldCollapseStudy) {
|
|
const madeInClient = true;
|
|
requestDisplaySetCreationForStudy(
|
|
DisplaySetService,
|
|
StudyInstanceUID,
|
|
madeInClient
|
|
);
|
|
}
|
|
}
|
|
|
|
const activeDisplaySetInstanceUIDs =
|
|
viewports[activeViewportIndex]?.displaySetInstanceUIDs;
|
|
|
|
return (
|
|
<StudyBrowser
|
|
tabs={tabs}
|
|
activeTabName={activeTabName}
|
|
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
|
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
|
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
|
onClickStudy={_handleStudyClick}
|
|
onClickTab={clickedTabName => {
|
|
setActiveTabName(clickedTabName);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
PanelStudyBrowser.propTypes = {
|
|
DisplaySetService: PropTypes.shape({
|
|
EVENTS: PropTypes.object.isRequired,
|
|
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
getDisplaySetByUID: PropTypes.func.isRequired,
|
|
subscribe: PropTypes.func.isRequired,
|
|
}).isRequired,
|
|
dataSource: PropTypes.shape({
|
|
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
|
}).isRequired,
|
|
getImageSrc: PropTypes.func.isRequired,
|
|
getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired,
|
|
requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
|
|
};
|
|
|
|
export default PanelStudyBrowser;
|
|
|
|
/**
|
|
* Maps from the DataSource's format to a naturalized object
|
|
*
|
|
* @param {*} studies
|
|
*/
|
|
function _mapDataSourceStudies(studies) {
|
|
return studies.map(study => {
|
|
// TODO: Why does the data source return in this format?
|
|
return {
|
|
AccessionNumber: study.accession,
|
|
StudyDate: study.date,
|
|
StudyDescription: study.description,
|
|
NumInstances: study.instances,
|
|
ModalitiesInStudy: study.modalities,
|
|
PatientID: study.mrn,
|
|
PatientName: study.patientName,
|
|
StudyInstanceUID: study.studyInstanceUid,
|
|
StudyTime: study.time,
|
|
};
|
|
});
|
|
}
|
|
|
|
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
|
const thumbnailDisplaySets = [];
|
|
const thumbnailNoImageDisplaySets = [];
|
|
|
|
displaySets.forEach(ds => {
|
|
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
|
const componentType = _getComponentType(ds.Modality);
|
|
|
|
const array =
|
|
componentType === 'thumbnail'
|
|
? thumbnailDisplaySets
|
|
: thumbnailNoImageDisplaySets;
|
|
|
|
array.push({
|
|
displaySetInstanceUID: ds.displaySetInstanceUID,
|
|
description: ds.SeriesDescription || '',
|
|
seriesNumber: ds.SeriesNumber,
|
|
modality: ds.Modality,
|
|
seriesDate: ds.SeriesDate,
|
|
numInstances: ds.numImageFrames,
|
|
StudyInstanceUID: ds.StudyInstanceUID,
|
|
componentType,
|
|
imageSrc,
|
|
dragData: {
|
|
type: 'displayset',
|
|
displaySetInstanceUID: ds.displaySetInstanceUID,
|
|
// .. Any other data to pass
|
|
},
|
|
});
|
|
});
|
|
|
|
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
|
|
}
|
|
|
|
const thumbnailNoImageModalities = [
|
|
'SR',
|
|
'SEG',
|
|
'RTSTRUCT',
|
|
'RTPLAN',
|
|
'RTDOSE',
|
|
];
|
|
|
|
function _getComponentType(Modality) {
|
|
if (thumbnailNoImageModalities.includes(Modality)) {
|
|
// TODO probably others.
|
|
return 'thumbnailNoImage';
|
|
}
|
|
|
|
return 'thumbnail';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string[]} primaryStudyInstanceUIDs
|
|
* @param {object[]} studyDisplayList
|
|
* @param {string} studyDisplayList.studyInstanceUid
|
|
* @param {string} studyDisplayList.date
|
|
* @param {string} studyDisplayList.description
|
|
* @param {string} studyDisplayList.modalities
|
|
* @param {number} studyDisplayList.numInstances
|
|
* @param {object[]} displaySets
|
|
* @returns tabs - The prop object expected by the StudyBrowser component
|
|
*/
|
|
function _createStudyBrowserTabs(
|
|
primaryStudyInstanceUIDs,
|
|
studyDisplayList,
|
|
displaySets
|
|
) {
|
|
const primaryStudies = [];
|
|
const recentStudies = [];
|
|
const allStudies = [];
|
|
|
|
studyDisplayList.forEach(study => {
|
|
const displaySetsForStudy = displaySets.filter(
|
|
ds => ds.StudyInstanceUID === study.studyInstanceUid
|
|
);
|
|
const tabStudy = Object.assign({}, study, {
|
|
displaySets: displaySetsForStudy,
|
|
});
|
|
|
|
if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) {
|
|
primaryStudies.push(tabStudy);
|
|
} else {
|
|
// TODO: Filter allStudies to dates within one year of current date
|
|
recentStudies.push(tabStudy);
|
|
allStudies.push(tabStudy);
|
|
}
|
|
});
|
|
|
|
const tabs = [
|
|
{
|
|
name: 'primary',
|
|
label: 'Primary',
|
|
studies: primaryStudies,
|
|
},
|
|
{
|
|
name: 'recent',
|
|
label: 'Recent',
|
|
studies: recentStudies,
|
|
},
|
|
{
|
|
name: 'all',
|
|
label: 'All',
|
|
studies: allStudies,
|
|
},
|
|
];
|
|
|
|
return tabs;
|
|
}
|