OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
@@ -5,7 +5,7 @@ import { Types } from '@ohif/core';
|
||||
export type SidePanelWithServicesProps = {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
side: 'left' | 'right';
|
||||
className: string;
|
||||
className?: string;
|
||||
activeTabIndex: number;
|
||||
tabs: any;
|
||||
expandedWidth?: number;
|
||||
|
||||
@@ -209,6 +209,10 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
return imageIds;
|
||||
},
|
||||
getImageIdsForInstance({ instance, frame }) {
|
||||
if (instance.imageId) {
|
||||
return instance.imageId;
|
||||
}
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
const storedInstance = DicomMetadataStore.getInstance(
|
||||
StudyInstanceUID,
|
||||
|
||||
@@ -2,8 +2,9 @@ import dcmjs from 'dcmjs';
|
||||
import moment from 'moment';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { classes } from '@ohif/core';
|
||||
import { InputRange, Select, Typography, InputFilterText } from '@ohif/ui';
|
||||
import { InputFilterText } from '@ohif/ui';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
|
||||
|
||||
import DicomTagTable from './DicomTagTable';
|
||||
import './DicomTagBrowser.css';
|
||||
@@ -57,7 +58,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
|
||||
return {
|
||||
value: displaySetInstanceUID,
|
||||
label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
|
||||
label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
|
||||
description: displayDate,
|
||||
};
|
||||
});
|
||||
@@ -107,62 +108,62 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dicom-tag-browser-content">
|
||||
<div className="mb-6 flex flex-row items-center pl-1">
|
||||
<div className="flex w-1/2 flex-row items-center">
|
||||
<Typography
|
||||
variant="subtitle"
|
||||
className="mr-4"
|
||||
>
|
||||
Series
|
||||
</Typography>
|
||||
<div className="mr-8 grow">
|
||||
<div className="dicom-tag-browser-content bg-muted">
|
||||
<div className="mb-6 flex flex-row items-start pl-1">
|
||||
<div className="flex w-full flex-row items-start gap-4">
|
||||
<div className="flex w-1/3 flex-col">
|
||||
<span className="text-muted-foreground flex h-6 items-center text-xs">Series</span>
|
||||
<Select
|
||||
id="display-set-selector"
|
||||
isClearable={false}
|
||||
onChange={onSelectChange}
|
||||
options={displaySetList}
|
||||
value={displaySetList.find(ds => ds.value === selectedDisplaySetInstanceUID)}
|
||||
className="text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/2 flex-row items-center">
|
||||
{showInstanceList && (
|
||||
<Typography
|
||||
variant="subtitle"
|
||||
className="mr-4"
|
||||
value={selectedDisplaySetInstanceUID}
|
||||
onValueChange={value => onSelectChange({ value })}
|
||||
>
|
||||
Instance Number
|
||||
</Typography>
|
||||
)}
|
||||
<SelectTrigger>
|
||||
{displaySetList.find(ds => ds.value === selectedDisplaySetInstanceUID)?.label ||
|
||||
'Select Series'}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{displaySetList.map(item => {
|
||||
return (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
<span className="text-muted-foreground ml-1 text-xs">{item.description}</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{showInstanceList && (
|
||||
<div className="grow">
|
||||
<InputRange
|
||||
value={instanceNumber}
|
||||
key={selectedDisplaySetInstanceUID}
|
||||
onChange={value => {
|
||||
setInstanceNumber(parseInt(value));
|
||||
<div className="mx-auto flex w-1/5 flex-col">
|
||||
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
||||
Instance Number ({instanceNumber} of {activeDisplaySet.images.length})
|
||||
</span>
|
||||
<Slider
|
||||
value={[instanceNumber]}
|
||||
onValueChange={([value]) => {
|
||||
setInstanceNumber(value);
|
||||
}}
|
||||
minValue={1}
|
||||
maxValue={activeDisplaySet.images.length}
|
||||
min={1}
|
||||
max={activeDisplaySet.images.length}
|
||||
step={1}
|
||||
inputClassName="w-full"
|
||||
labelPosition="left"
|
||||
trackColor={'#3a3f99'}
|
||||
className="pt-4"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-auto flex w-1/3 flex-col">
|
||||
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
||||
Search metadata
|
||||
</span>
|
||||
<InputFilterText
|
||||
placeholder="Search metadata..."
|
||||
onDebounceChange={setFilterValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-black"></div>
|
||||
<div className="my-3 flex w-1/2 flex-row">
|
||||
<InputFilterText
|
||||
className="mr-8 block w-full"
|
||||
placeholder="Search metadata..."
|
||||
onDebounceChange={setFilterValue}
|
||||
></InputFilterText>
|
||||
</div>
|
||||
<DicomTagTable rows={filteredRows} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -154,9 +154,11 @@ function mapParams(params, options = {}) {
|
||||
// Add more fields here if you want them in the result
|
||||
].join(',');
|
||||
|
||||
const { supportsWildcard } = options;
|
||||
const useWildcard =
|
||||
params?.disableWildcard !== undefined ? !params.disableWildcard : options.supportsWildcard;
|
||||
|
||||
const withWildcard = value => {
|
||||
return supportsWildcard && value ? `*${value}*` : value;
|
||||
return useWildcard && value ? `*${value}*` : value;
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
|
||||
@@ -29,6 +29,10 @@ export default function getImageId({ instance, frame, config, thumbnail = false
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.imageId && frame === undefined) {
|
||||
return instance.imageId;
|
||||
}
|
||||
|
||||
if (instance.url) {
|
||||
return instance.url;
|
||||
}
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
MeasurementTable,
|
||||
Dialog,
|
||||
Input,
|
||||
useViewportGrid,
|
||||
ButtonEnums,
|
||||
ActionButtons,
|
||||
} from '@ohif/ui';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import createReportDialogPrompt, {
|
||||
CREATE_REPORT_DIALOG_RESPONSE,
|
||||
} from './createReportDialogPrompt';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
|
||||
export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}: withAppTypes): React.FunctionComponent {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { activeViewportId, viewports } = viewportGrid;
|
||||
const { measurementService, uiDialogService, uiNotificationService, displaySetService } =
|
||||
servicesManager.services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const debouncedSetDisplayMeasurements = debounce(setDisplayMeasurements, 100);
|
||||
// ~~ Initial
|
||||
setDisplayMeasurements(_getMappedMeasurements(measurementService));
|
||||
|
||||
// ~~ Subscription
|
||||
const added = measurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const updated = measurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||
const removed = measurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||
const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||
subscriptions.push(
|
||||
measurementService.subscribe(evt, () => {
|
||||
debouncedSetDisplayMeasurements(_getMappedMeasurements(measurementService));
|
||||
}).unsubscribe
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
debouncedSetDisplayMeasurements.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function exportReport() {
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
downloadCSVReport(measurements, measurementService);
|
||||
}
|
||||
|
||||
async function clearMeasurements() {
|
||||
measurementService.clearMeasurements();
|
||||
}
|
||||
|
||||
async function createReport(): Promise<any> {
|
||||
// filter measurements that are added to the active study
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
const measurements = measurementService.getMeasurements();
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
activeViewport.displaySetInstanceUIDs[0]
|
||||
);
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m => displaySet.StudyInstanceUID === m.referenceStudyUID
|
||||
);
|
||||
|
||||
if (trackedMeasurements.length <= 0) {
|
||||
uiNotificationService.show({
|
||||
title: 'No Measurements',
|
||||
message: 'No Measurements are added to the current Study.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources(promptResult.dataSourceName);
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
const SeriesDescription =
|
||||
// isUndefinedOrEmpty
|
||||
promptResult.value === undefined || promptResult.value === ''
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
// Reuse an existing series having the same series description to avoid
|
||||
// creating too many series instances.
|
||||
const options = findSRWithSameSeriesDescription(SeriesDescription, displaySetService);
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: trackedMeasurements,
|
||||
dataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options,
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
};
|
||||
|
||||
return createReportAsync({ servicesManager, getReport });
|
||||
}
|
||||
}
|
||||
|
||||
const jumpToImage = ({ uid, isActive }) => {
|
||||
measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
|
||||
|
||||
onMeasurementItemClickHandler({ uid, isActive });
|
||||
};
|
||||
|
||||
const onMeasurementItemEditHandler = ({ uid, isActive }) => {
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
//Todo: why we are jumping to image?
|
||||
// jumpToImage({ id, isActive });
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
measurementService.update(
|
||||
uid,
|
||||
{
|
||||
...measurement,
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'enter-annotation' });
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'enter-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Annotation',
|
||||
noCloseButton: true,
|
||||
value: { label: measurement.label || '' },
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Input
|
||||
label="Enter your annotation"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
autoFocus
|
||||
id="annotation"
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
/>
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onMeasurementItemClickHandler = ({ uid, isActive }) => {
|
||||
if (!isActive) {
|
||||
const measurements = [...displayMeasurements];
|
||||
const measurement = measurements.find(m => m.uid === uid);
|
||||
|
||||
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
|
||||
measurement.isActive = true;
|
||||
setDisplayMeasurements(measurements);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="ohif-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
data-cy={'measurements-panel'}
|
||||
>
|
||||
<MeasurementTable
|
||||
title={t('Measurements')}
|
||||
servicesManager={servicesManager}
|
||||
data={displayMeasurements}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center p-4">
|
||||
<ActionButtons
|
||||
t={t}
|
||||
actions={[
|
||||
{
|
||||
label: 'Export',
|
||||
onClick: exportReport,
|
||||
},
|
||||
{
|
||||
label: 'Create Report',
|
||||
onClick: createReport,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PanelMeasurementTable.propTypes = {
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function _getMappedMeasurements(measurementService) {
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
const mappedMeasurements = measurements.map((m, index) =>
|
||||
_mapMeasurementToDisplay(m, index, measurementService.VALUE_TYPES)
|
||||
);
|
||||
|
||||
return mappedMeasurements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the measurements to the display text.
|
||||
* Adds finding and site information to the displayText and/or label,
|
||||
* and provides as 'displayText' and 'label', while providing the original
|
||||
* values as baseDisplayText and baseLabel
|
||||
*/
|
||||
function _mapMeasurementToDisplay(measurement, index, types) {
|
||||
const {
|
||||
displayText: baseDisplayText,
|
||||
uid,
|
||||
label: baseLabel,
|
||||
type,
|
||||
selected,
|
||||
findingSites,
|
||||
finding,
|
||||
} = measurement;
|
||||
|
||||
const firstSite = findingSites?.[0];
|
||||
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
|
||||
let displayText = baseDisplayText || [];
|
||||
if (findingSites) {
|
||||
const siteText = [];
|
||||
findingSites.forEach(site => {
|
||||
if (site?.text !== label) {
|
||||
siteText.push(site.text);
|
||||
}
|
||||
});
|
||||
displayText = [...siteText, ...displayText];
|
||||
}
|
||||
if (finding && finding?.text !== label) {
|
||||
displayText = [finding.text, ...displayText];
|
||||
}
|
||||
|
||||
return {
|
||||
uid,
|
||||
label,
|
||||
baseLabel,
|
||||
measurementType: type,
|
||||
displayText,
|
||||
baseDisplayText,
|
||||
isActive: selected,
|
||||
finding,
|
||||
findingSites,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { StudyBrowser } from '@ohif/ui-next';
|
||||
import { utils } from '@ohif/core';
|
||||
@@ -80,7 +79,7 @@ function PanelStudyBrowser({
|
||||
uiNotificationService.show({
|
||||
title: 'Thumbnail Double Click',
|
||||
message: 'The selected display sets could not be added to the viewport.',
|
||||
type: 'info',
|
||||
type: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
@@ -198,7 +197,7 @@ function PanelStudyBrowser({
|
||||
// if (!hasLoadedViewports) {
|
||||
// return;
|
||||
// }
|
||||
const { displaySetsAdded, options } = data;
|
||||
const { displaySetsAdded } = data;
|
||||
displaySetsAdded.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||
@@ -319,16 +318,6 @@ function PanelStudyBrowser({
|
||||
);
|
||||
}
|
||||
|
||||
PanelStudyBrowser.propTypes = {
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getImageSrc: PropTypes.func.isRequired,
|
||||
getStudiesForPatientByMRN: PropTypes.func.isRequired,
|
||||
requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PanelStudyBrowser;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui';
|
||||
|
||||
export const CREATE_REPORT_DIALOG_RESPONSE = {
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
};
|
||||
import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
@@ -16,7 +12,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
// Notify of cancel action
|
||||
resolve({
|
||||
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
@@ -32,14 +28,14 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
resolve({
|
||||
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
dataSourceName: value.dataSourceName,
|
||||
});
|
||||
break;
|
||||
case 'cancel':
|
||||
resolve({
|
||||
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
@@ -90,7 +86,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
|
||||
if (event.key === 'Enter') {
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
resolve({
|
||||
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
});
|
||||
}
|
||||
@@ -102,7 +98,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
|
||||
<label className="text-[14px] leading-[1.2] text-white">Data Source</label>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mt-2 bg-black"
|
||||
className="border-primary-main mt-2 bg-black"
|
||||
options={dataSourcesOpts}
|
||||
placeholder={
|
||||
dataSourcesOpts.find(option => option.value === value.dataSourceName)
|
||||
|
||||
@@ -2,6 +2,7 @@ async function getStudiesForPatientByMRN(dataSource, qidoForStudyUID) {
|
||||
if (qidoForStudyUID && qidoForStudyUID.length && qidoForStudyUID[0].mrn) {
|
||||
return dataSource.query.studies.search({
|
||||
patientId: qidoForStudyUID[0].mrn,
|
||||
disableWildcard: true,
|
||||
});
|
||||
}
|
||||
console.log('No mrn found for', qidoForStudyUID);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable';
|
||||
import createReportDialogPrompt from './createReportDialogPrompt';
|
||||
|
||||
export {
|
||||
PanelStudyBrowser,
|
||||
WrappedPanelStudyBrowser,
|
||||
PanelMeasurementTable,
|
||||
createReportDialogPrompt,
|
||||
};
|
||||
export { PanelStudyBrowser, WrappedPanelStudyBrowser, createReportDialogPrompt };
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import usePatientInfo from '../../hooks/usePatientInfo';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
|
||||
export enum PatientInfoVisibility {
|
||||
VISIBLE = 'visible',
|
||||
VISIBLE_COLLAPSED = 'visibleCollapsed',
|
||||
DISABLED = 'disabled',
|
||||
VISIBLE_READONLY = 'visibleReadOnly',
|
||||
}
|
||||
|
||||
const formatWithEllipsis = (str, maxLength) => {
|
||||
if (str?.length > maxLength) {
|
||||
return str.substring(0, maxLength) + '...';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
function HeaderPatientInfo({ servicesManager, appConfig }: withAppTypes) {
|
||||
const initialExpandedState =
|
||||
appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE ||
|
||||
appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE_READONLY;
|
||||
const [expanded, setExpanded] = useState(initialExpandedState);
|
||||
const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMixedPatients && expanded) {
|
||||
setExpanded(false);
|
||||
}
|
||||
}, [isMixedPatients, expanded]);
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (!isMixedPatients && appConfig.showPatientInfo !== PatientInfoVisibility.VISIBLE_READONLY) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
|
||||
const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27);
|
||||
const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-primary-dark flex cursor-pointer items-center justify-center gap-1 rounded-lg"
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{isMixedPatients ? (
|
||||
<Icons.MultiplePatients className="text-primary-active" />
|
||||
) : (
|
||||
<Icons.Patient className="text-primary-active" />
|
||||
)}
|
||||
<div className="flex flex-col justify-center">
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className="self-start text-[13px] font-bold text-white">
|
||||
{formattedPatientName}
|
||||
</div>
|
||||
<div className="text-aqua-pale flex gap-2 text-[11px]">
|
||||
<div>{formattedPatientID}</div>
|
||||
<div>{patientInfo.PatientSex}</div>
|
||||
<div>{patientInfo.PatientDOB}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-primary-active self-center text-[13px]">
|
||||
{isMixedPatients ? 'Multiple Patients' : 'Patient'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Icons.ChevronPatient className={`text-primary-active ${expanded ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeaderPatientInfo;
|
||||
@@ -0,0 +1,3 @@
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
|
||||
export default HeaderPatientInfo;
|
||||
@@ -3,10 +3,13 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
|
||||
import { UserPreferences, AboutModal, useModal } from '@ohif/ui';
|
||||
import { Header } from '@ohif/ui-next';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
@@ -15,7 +18,7 @@ function ViewerHeader({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
appConfig,
|
||||
}: withAppTypes) {
|
||||
}: withAppTypes<{ appConfig: AppTypes.Config }>) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -110,21 +113,24 @@ function ViewerHeader({
|
||||
isReturnEnabled={!!appConfig.showStudyList}
|
||||
onClickReturnButton={onClickReturnButton}
|
||||
WhiteLabeling={appConfig.whiteLabeling}
|
||||
showPatientInfo={appConfig.showPatientInfo}
|
||||
servicesManager={servicesManager}
|
||||
Secondary={
|
||||
<Toolbar
|
||||
servicesManager={servicesManager}
|
||||
buttonSection="secondary"
|
||||
/>
|
||||
}
|
||||
appConfig={appConfig}
|
||||
PatientInfo={
|
||||
appConfig.showPatientInfo !== PatientInfoVisibility.DISABLED && (
|
||||
<HeaderPatientInfo
|
||||
servicesManager={servicesManager}
|
||||
appConfig={appConfig}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ErrorBoundary context="Primary Toolbar">
|
||||
<div className="relative flex justify-center gap-[4px]">
|
||||
<Toolbar servicesManager={servicesManager} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<div className="relative flex justify-center gap-[4px]">
|
||||
<Toolbar servicesManager={servicesManager} />
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ErrorBoundary, LoadingIndicatorProgress, InvestigationalUseDialog } from '@ohif/ui';
|
||||
import { LoadingIndicatorProgress, InvestigationalUseDialog } from '@ohif/ui';
|
||||
import { HangingProtocolService, CommandsManager } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import ViewerHeader from './ViewerHeader';
|
||||
@@ -125,34 +125,28 @@ function ViewerLayout({
|
||||
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-black" />}
|
||||
{/* LEFT SIDEPANELS */}
|
||||
{hasLeftPanels ? (
|
||||
<ErrorBoundary context="Left Panel">
|
||||
<SidePanelWithServices
|
||||
side="left"
|
||||
activeTabIndex={leftPanelClosedState ? null : 0}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<SidePanelWithServices
|
||||
side="left"
|
||||
activeTabIndex={leftPanelClosedState ? null : 0}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
) : null}
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex h-full flex-1 flex-col">
|
||||
<div className="relative flex h-full flex-1 items-center justify-center overflow-hidden bg-black">
|
||||
<ErrorBoundary context="Grid">
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
viewportComponents={viewportComponents}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
viewportComponents={viewportComponents}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hasRightPanels ? (
|
||||
<ErrorBoundary context="Right Panel">
|
||||
<SidePanelWithServices
|
||||
side="right"
|
||||
activeTabIndex={rightPanelClosedState ? null : 0}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<SidePanelWithServices
|
||||
side="right"
|
||||
activeTabIndex={rightPanelClosedState ? null : 0}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { utils, Types } from '@ohif/core';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
@@ -10,8 +10,13 @@ import findViewportsByPosition, {
|
||||
import { ContextMenuProps } from './CustomizableContextMenu/types';
|
||||
import { NavigateHistory } from './types/commandModuleTypes';
|
||||
import { history } from '@ohif/app';
|
||||
import { useViewportGridStore } from './stores/useViewportGridStore';
|
||||
import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
|
||||
import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
|
||||
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
||||
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||
|
||||
const { subscribeToNextViewportGridChange } = utils;
|
||||
export type HangingProtocolParams = {
|
||||
protocolId?: string;
|
||||
stageIndex?: number;
|
||||
@@ -36,7 +41,6 @@ const commandsModule = ({
|
||||
uiNotificationService,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// Define a context menu controller for use with any context menus
|
||||
@@ -134,9 +138,9 @@ const commandsModule = ({
|
||||
// the activeViewportId
|
||||
const state = viewportGridService.getState();
|
||||
const hpInfo = hangingProtocolService.getState();
|
||||
const stateSyncReduce = reuseCachedLayouts(state, hangingProtocolService, stateSyncService);
|
||||
const { hangingProtocolStageIndexMap, viewportGridStore, displaySetSelectorMap } =
|
||||
stateSyncReduce;
|
||||
reuseCachedLayouts(state, hangingProtocolService);
|
||||
const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState();
|
||||
const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState();
|
||||
|
||||
if (!protocolId) {
|
||||
// Reuse the previous protocol id, and optionally stage
|
||||
@@ -165,7 +169,8 @@ const commandsModule = ({
|
||||
useStageIdx || 0
|
||||
}`;
|
||||
|
||||
const restoreProtocol = !reset && viewportGridStore[storedHanging];
|
||||
const { viewportGridState } = useViewportGridStore.getState();
|
||||
const restoreProtocol = !reset && viewportGridState[storedHanging];
|
||||
|
||||
if (
|
||||
protocolId === hpInfo.protocolId &&
|
||||
@@ -185,17 +190,15 @@ const commandsModule = ({
|
||||
restoreProtocol,
|
||||
});
|
||||
if (restoreProtocol) {
|
||||
viewportGridService.set(viewportGridStore[storedHanging]);
|
||||
viewportGridService.set(viewportGridState[storedHanging]);
|
||||
}
|
||||
}
|
||||
// Do this after successfully applying the update
|
||||
// Note, don't store the active display set - it is only needed while
|
||||
// changing display sets. This causes jump to measurement to fail on
|
||||
// multi-study display.
|
||||
delete displaySetSelectorMap[
|
||||
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`
|
||||
];
|
||||
stateSyncService.store(stateSyncReduce);
|
||||
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
||||
setDisplaySetSelector(
|
||||
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
|
||||
null
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -215,7 +218,8 @@ const commandsModule = ({
|
||||
stageIndex: desiredStageIndex,
|
||||
activeStudy,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const { toggleHangingProtocol } = stateSyncService.getState();
|
||||
const { toggleHangingProtocol, setToggleHangingProtocol } =
|
||||
useToggleHangingProtocolStore.getState();
|
||||
const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`;
|
||||
if (
|
||||
protocol.id === protocolId &&
|
||||
@@ -227,14 +231,9 @@ const commandsModule = ({
|
||||
};
|
||||
return actions.setHangingProtocol(previousState);
|
||||
} else {
|
||||
stateSyncService.store({
|
||||
toggleHangingProtocol: {
|
||||
...toggleHangingProtocol,
|
||||
[storedHanging]: {
|
||||
protocolId: protocol.id,
|
||||
stageIndex: desiredStageIndex,
|
||||
},
|
||||
},
|
||||
setToggleHangingProtocol(storedHanging, {
|
||||
protocolId: protocol.id,
|
||||
stageIndex: desiredStageIndex,
|
||||
});
|
||||
return actions.setHangingProtocol({
|
||||
protocolId,
|
||||
@@ -281,12 +280,15 @@ const commandsModule = ({
|
||||
|
||||
const completeLayout = () => {
|
||||
const state = viewportGridService.getState();
|
||||
const stateReduce = findViewportsByPosition(state, { numRows, numCols }, stateSyncService);
|
||||
findViewportsByPosition(state, { numRows, numCols });
|
||||
|
||||
const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState();
|
||||
|
||||
const findOrCreateViewport = layoutFindOrCreate.bind(
|
||||
null,
|
||||
hangingProtocolService,
|
||||
isHangingProtocolLayout,
|
||||
stateReduce.viewportsByPosition
|
||||
{ ...viewportsByPosition, initialInDisplay }
|
||||
);
|
||||
|
||||
viewportGridService.setLayout({
|
||||
@@ -295,7 +297,6 @@ const commandsModule = ({
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
});
|
||||
stateSyncService.store(stateReduce);
|
||||
};
|
||||
// Need to finish any work in the callback
|
||||
window.setTimeout(completeLayout, 0);
|
||||
@@ -309,9 +310,9 @@ const commandsModule = ({
|
||||
|
||||
if (layout.numCols === 1 && layout.numRows === 1) {
|
||||
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
|
||||
const { toggleOneUpViewportGridStore } = stateSyncService.getState();
|
||||
const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
||||
|
||||
if (!toggleOneUpViewportGridStore.layout) {
|
||||
if (!toggleOneUpViewportGridStore) {
|
||||
return;
|
||||
}
|
||||
// There is a state to toggle back to. The viewport that was
|
||||
@@ -374,11 +375,9 @@ const commandsModule = ({
|
||||
// We are not in one-up, so toggle to one up.
|
||||
|
||||
// Store the current viewport grid state so we can toggle it back later.
|
||||
stateSyncService.store({
|
||||
toggleOneUpViewportGridStore: viewportGridState,
|
||||
});
|
||||
const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
||||
setToggleOneUpViewportGridStore(viewportGridState);
|
||||
|
||||
// This findOrCreateViewport only return one viewport - the active
|
||||
// one being toggled to one up.
|
||||
const findOrCreateViewport = () => {
|
||||
return {
|
||||
@@ -395,19 +394,6 @@ const commandsModule = ({
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout: true,
|
||||
});
|
||||
|
||||
// Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
|
||||
// any grid layout state to toggle to from one up is cleared. This is performed on
|
||||
// a timeout to avoid clearing the state for the actual to one up change.
|
||||
// Whenever the next layout change event is fired, the subscriptions are unsubscribed.
|
||||
const clearToggleOneUpViewportGridStore = () => {
|
||||
const toggleOneUpViewportGridStore = {};
|
||||
stateSyncService.store({
|
||||
toggleOneUpViewportGridStore,
|
||||
});
|
||||
};
|
||||
|
||||
subscribeToNextViewportGridChange(viewportGridService, clearToggleOneUpViewportGridStore);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StateSyncService } from '@ohif/core';
|
||||
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
||||
|
||||
/**
|
||||
* This find or create viewport is paired with the reduce results from
|
||||
@@ -69,16 +69,12 @@ export const findOrCreateViewport = (
|
||||
* @returns Set of states that can be applied to the state sync to remember
|
||||
* the current view state.
|
||||
*/
|
||||
const findViewportsByPosition = (
|
||||
state,
|
||||
{ numRows, numCols },
|
||||
syncService: StateSyncService
|
||||
): Record<string, Record<string, unknown>> => {
|
||||
const findViewportsByPosition = (state, { numRows, numCols }) => {
|
||||
const { viewports } = state;
|
||||
const syncState = syncService.getState();
|
||||
const viewportsByPosition = { ...syncState.viewportsByPosition };
|
||||
const { setViewportsByPosition, addInitialInDisplay } = useViewportsByPositionStore.getState();
|
||||
const initialInDisplay = [];
|
||||
|
||||
const viewportsByPosition = {};
|
||||
viewports.forEach(viewport => {
|
||||
if (viewport.positionId) {
|
||||
const storedViewport = {
|
||||
@@ -86,6 +82,7 @@ const findViewportsByPosition = (
|
||||
viewportOptions: { ...viewport.viewportOptions },
|
||||
};
|
||||
viewportsByPosition[viewport.positionId] = storedViewport;
|
||||
setViewportsByPosition(viewport.positionId, storedViewport);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -99,10 +96,7 @@ const findViewportsByPosition = (
|
||||
}
|
||||
}
|
||||
|
||||
// Store the initially displayed elements
|
||||
viewportsByPosition.initialInDisplay = initialInDisplay;
|
||||
|
||||
return { viewportsByPosition };
|
||||
initialInDisplay.forEach(displaySetInstanceUID => addInitialInDisplay(displaySetInstanceUID));
|
||||
};
|
||||
|
||||
export default findViewportsByPosition;
|
||||
@@ -27,6 +27,17 @@ const defaultProtocol = {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@@ -58,8 +69,6 @@ const defaultProtocol = {
|
||||
},
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
@@ -88,6 +97,17 @@ const defaultProtocol = {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WrappedPanelStudyBrowser, PanelMeasurementTable } from './Panels';
|
||||
import { WrappedPanelStudyBrowser } from './Panels';
|
||||
import i18n from 'i18next';
|
||||
|
||||
// TODO:
|
||||
@@ -8,17 +8,6 @@ import i18n from 'i18next';
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedMeasurementPanel = ({ tab }) => {
|
||||
return (
|
||||
<PanelMeasurementTable
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'seriesList',
|
||||
@@ -34,14 +23,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'measurements',
|
||||
iconName: 'tab-linear',
|
||||
iconLabel: 'Measure',
|
||||
label: i18n.t('SidePanel:Measurements'),
|
||||
secondaryLabel: i18n.t('SidePanel:Measurements'),
|
||||
component: wrappedMeasurementPanel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Sync group configuration for hydrating segmentations across viewports
|
||||
* that share the same frame of reference
|
||||
* @type {Types.HangingProtocol.SyncGroup}
|
||||
*/
|
||||
export const HYDRATE_SEG_SYNC_GROUP = {
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* This hanging protocol can be activated on the primary mode by directly
|
||||
* referencing it in a URL or by directly including it within a mode, e.g.:
|
||||
@@ -48,6 +63,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [HYDRATE_SEG_SYNC_GROUP],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@@ -59,6 +75,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
stages: [
|
||||
{
|
||||
id: '2x2',
|
||||
name: '2x2',
|
||||
stageActivation: {
|
||||
enabled: {
|
||||
minViewportsMatched: 4,
|
||||
@@ -76,6 +93,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@@ -99,6 +127,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@@ -111,6 +150,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { formatPN, formatDate } = utils;
|
||||
|
||||
function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
const [patientInfo, setPatientInfo] = useState({
|
||||
PatientName: '',
|
||||
PatientID: '',
|
||||
PatientSex: '',
|
||||
PatientDOB: '',
|
||||
});
|
||||
const [isMixedPatients, setIsMixedPatients] = useState(false);
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const checkMixedPatients = PatientID => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
let isMixedPatients = false;
|
||||
displaySets.forEach(displaySet => {
|
||||
const instance = displaySet?.instances?.[0] || displaySet?.instance;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
if (instance.PatientID !== PatientID) {
|
||||
isMixedPatients = true;
|
||||
}
|
||||
});
|
||||
setIsMixedPatients(isMixedPatients);
|
||||
};
|
||||
|
||||
const updatePatientInfo = () => {
|
||||
const displaySet = displaySets[0];
|
||||
const instance = displaySet?.instances?.[0] || displaySet?.instance;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
setPatientInfo({
|
||||
PatientID: instance.PatientID || null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null,
|
||||
PatientSex: instance.PatientSex || null,
|
||||
PatientDOB: formatDate(instance.PatientBirthDate) || null,
|
||||
});
|
||||
checkMixedPatients(instance.PatientID || null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
() => updatePatientInfo()
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updatePatientInfo();
|
||||
}, [displaySets]);
|
||||
|
||||
return { patientInfo, isMixedPatients };
|
||||
}
|
||||
|
||||
export default usePatientInfo;
|
||||
@@ -18,6 +18,23 @@ import { createReportDialogPrompt } from './Panels';
|
||||
import createReportAsync from './Actions/createReportAsync';
|
||||
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
|
||||
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
|
||||
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
||||
import { useViewportGridStore } from './stores/useViewportGridStore';
|
||||
import { useUIStateStore } from './stores/useUIStateStore';
|
||||
import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
|
||||
import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
|
||||
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||
import {
|
||||
callLabelAutocompleteDialog,
|
||||
showLabelAnnotationPopup,
|
||||
callInputDialog,
|
||||
} from './utils/callInputDialog';
|
||||
import colorPickerDialog from './utils/colorPickerDialog';
|
||||
|
||||
import promptSaveReport from './utils/promptSaveReport';
|
||||
import promptLabelAnnotation from './utils/promptLabelAnnotation';
|
||||
import usePatientInfo from './hooks/usePatientInfo';
|
||||
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
/**
|
||||
@@ -25,6 +42,14 @@ const defaultExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
preRegistration,
|
||||
onModeExit() {
|
||||
useViewportGridStore.getState().clearViewportGridState();
|
||||
useUIStateStore.getState().clearUIState();
|
||||
useDisplaySetSelectorStore.getState().clearDisplaySetSelectorMap();
|
||||
useHangingProtocolStageIndexStore.getState().clearHangingProtocolStageIndexMap();
|
||||
useToggleHangingProtocolStore.getState().clearToggleHangingProtocol();
|
||||
useViewportsByPositionStore.getState().clearViewportsByPosition();
|
||||
},
|
||||
getDataSourcesModule,
|
||||
getViewportModule,
|
||||
getLayoutTemplateModule,
|
||||
@@ -58,4 +83,19 @@ export {
|
||||
createReportAsync,
|
||||
StaticWadoClient,
|
||||
cleanDenaturalizedDataset,
|
||||
// Export all stores
|
||||
useDisplaySetSelectorStore,
|
||||
useHangingProtocolStageIndexStore,
|
||||
useToggleHangingProtocolStore,
|
||||
useToggleOneUpViewportGridStore,
|
||||
useUIStateStore,
|
||||
useViewportGridStore,
|
||||
useViewportsByPositionStore,
|
||||
showLabelAnnotationPopup,
|
||||
callLabelAutocompleteDialog,
|
||||
callInputDialog,
|
||||
promptSaveReport,
|
||||
promptLabelAnnotation,
|
||||
colorPickerDialog,
|
||||
usePatientInfo,
|
||||
};
|
||||
@@ -16,8 +16,7 @@ export default function init({
|
||||
configuration = {},
|
||||
commandsManager,
|
||||
}: withAppTypes): void {
|
||||
const { stateSyncService, toolbarService, cineService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
const { toolbarService, cineService, viewportGridService } = servicesManager.services;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cineService, [
|
||||
cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
@@ -29,38 +28,6 @@ export default function init({
|
||||
// we need to recalculate the SUV Scaling Factors
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata);
|
||||
|
||||
// viewportGridStore is a sync state which stores the entire
|
||||
// ViewportGridService getState, by the keys `<activeStudyUID>:<protocolId>:<stageIndex>`
|
||||
// Used to recover manual changes to the layout of a stage.
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
|
||||
// uiStateStore is a sync state which stores the relevant
|
||||
// UI state for the viewer
|
||||
stateSyncService.register('uiStateStore', { clearOnModeExit: true });
|
||||
|
||||
// displaySetSelectorMap stores a map from
|
||||
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
|
||||
// a displaySetInstanceUID, used to display named display sets in
|
||||
// specific spots within a hanging protocol and be able to remember what the
|
||||
// user did with those named spots between stages and protocols.
|
||||
stateSyncService.register('displaySetSelectorMap', { clearOnModeExit: true });
|
||||
|
||||
// Stores a map from `<activeStudyUID>:${protocolId}` to the getHPInfo results
|
||||
// in order to recover the correct stage when returning to a Hanging Protocol.
|
||||
stateSyncService.register('hangingProtocolStageIndexMap', {
|
||||
clearOnModeExit: true,
|
||||
});
|
||||
|
||||
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
|
||||
// to the previously applied hanging protocolStageIndexMap key, in order to toggle
|
||||
// off the applied protocol and remember the old state.
|
||||
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
|
||||
|
||||
// Stores the viewports by `rows-cols` position so that when the layout
|
||||
// changes numRows and numCols, the viewports can be remembers and then replaced
|
||||
// afterwards.
|
||||
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
|
||||
|
||||
// Adds extra custom attributes for use by hanging protocols
|
||||
registerHangingProtocolAttributes({ servicesManager });
|
||||
|
||||
@@ -119,7 +86,6 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
const imageIds = instances.map(instance => instance.imageId);
|
||||
const instanceMetadataArray = [];
|
||||
|
||||
// try except block to prevent errors when the metadata is not correct
|
||||
try {
|
||||
imageIds.forEach(imageId => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export { useDisplaySetSelectorStore } from './useDisplaySetSelectorStore';
|
||||
export { useHangingProtocolStageIndexStore } from './useHangingProtocolStageIndexStore';
|
||||
export { useToggleHangingProtocolStore } from './useToggleHangingProtocolStore';
|
||||
export { useToggleOneUpViewportGridStore } from './useToggleOneUpViewportGridStore';
|
||||
export { useUIStateStore } from './useUIStateStore';
|
||||
export { useViewportGridStore } from './useViewportGridStore';
|
||||
export { useViewportsByPositionStore } from './useViewportsByPositionStore';
|
||||
@@ -0,0 +1,83 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
/**
|
||||
* Identifier for the display set selector store type.
|
||||
*/
|
||||
const PRESENTATION_TYPE_ID = 'displaySetSelectorId';
|
||||
|
||||
/**
|
||||
* Flag to enable or disable debug mode for the store.
|
||||
* Set to `true` to enable zustand devtools.
|
||||
*/
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* State shape for the Display Set Selector store.
|
||||
*/
|
||||
type DisplaySetSelectorState = {
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Stores a mapping from `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to `displaySetInstanceUID`.
|
||||
*/
|
||||
displaySetSelectorMap: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Sets the display set selector for a given key.
|
||||
*
|
||||
* @param key - The key.
|
||||
* @param value - The `displaySetInstanceUID` to associate with the key.
|
||||
*/
|
||||
setDisplaySetSelector: (key: string, value: string) => void;
|
||||
|
||||
/**
|
||||
* Clears the entire display set selector map.
|
||||
*/
|
||||
clearDisplaySetSelectorMap: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Display Set Selector store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The display set selector store state and actions.
|
||||
*/
|
||||
const createDisplaySetSelectorStore = (set): DisplaySetSelectorState => ({
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
displaySetSelectorMap: {},
|
||||
|
||||
/**
|
||||
* Sets the display set selector for a given key.
|
||||
*/
|
||||
setDisplaySetSelector: (key: string, value: string) =>
|
||||
set(
|
||||
state => ({
|
||||
displaySetSelectorMap: {
|
||||
...state.displaySetSelectorMap,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setDisplaySetSelector'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears the entire display set selector map.
|
||||
*/
|
||||
clearDisplaySetSelectorMap: () =>
|
||||
set({ displaySetSelectorMap: {} }, false, 'clearDisplaySetSelectorMap'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing display set selectors.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useDisplaySetSelectorStore = create<DisplaySetSelectorState>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createDisplaySetSelectorStore, { name: 'DisplaySetSelectorStore' })
|
||||
: createDisplaySetSelectorStore
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'hangingProtocolStageIndexId';
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing hanging protocol stage indexes.
|
||||
*/
|
||||
type HangingProtocolStageIndexState = {
|
||||
/**
|
||||
* Stores a mapping from key to `HPInfo`.
|
||||
*/
|
||||
hangingProtocolStageIndexMap: Record<string, Types.HangingProtocol.HPInfo>;
|
||||
|
||||
/**
|
||||
* Sets the hanging protocol stage index for a given key.
|
||||
*
|
||||
* @param key - The key.
|
||||
* @param value - The `HPInfo` to associate with the key.
|
||||
*/
|
||||
setHangingProtocolStageIndex: (key: string, value: Types.HangingProtocol.HPInfo) => void;
|
||||
|
||||
/**
|
||||
* Clears all hanging protocol stage indexes.
|
||||
*/
|
||||
clearHangingProtocolStageIndexMap: () => void;
|
||||
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Hanging Protocol Stage Index store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The hanging protocol stage index store state and actions.
|
||||
*/
|
||||
const createHangingProtocolStageIndexStore = (set): HangingProtocolStageIndexState => ({
|
||||
hangingProtocolStageIndexMap: {},
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
|
||||
/**
|
||||
* Sets the hanging protocol stage index for a given key.
|
||||
*/
|
||||
setHangingProtocolStageIndex: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
hangingProtocolStageIndexMap: {
|
||||
...state.hangingProtocolStageIndexMap,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setHangingProtocolStageIndex'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears all hanging protocol stage indexes.
|
||||
*/
|
||||
clearHangingProtocolStageIndexMap: () =>
|
||||
set({ hangingProtocolStageIndexMap: {} }, false, 'clearHangingProtocolStageIndexMap'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing hanging protocol stage indexes.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useHangingProtocolStageIndexStore = create<HangingProtocolStageIndexState>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createHangingProtocolStageIndexStore, { name: 'HangingProtocolStageIndexStore' })
|
||||
: createHangingProtocolStageIndexStore
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'toggleHangingProtocolId';
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing toggle hanging protocols.
|
||||
*/
|
||||
type ToggleHangingProtocolState = {
|
||||
/**
|
||||
* Stores a mapping from key to `HPInfo`.
|
||||
*/
|
||||
toggleHangingProtocol: Record<string, Types.HangingProtocol.HPInfo>;
|
||||
|
||||
/**
|
||||
* Sets the toggle hanging protocol for a given key.
|
||||
*
|
||||
* @param key - The key .
|
||||
* @param value - The `HPInfo` to associate with the key.
|
||||
*/
|
||||
setToggleHangingProtocol: (key: string, value: Types.HangingProtocol.HPInfo) => void;
|
||||
|
||||
/**
|
||||
* Clears all toggle hanging protocols.
|
||||
*/
|
||||
clearToggleHangingProtocol: () => void;
|
||||
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Toggle Hanging Protocol store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The toggle hanging protocol store state and actions.
|
||||
*/
|
||||
const createToggleHangingProtocolStore = (set): ToggleHangingProtocolState => ({
|
||||
toggleHangingProtocol: {},
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
|
||||
/**
|
||||
* Sets the toggle hanging protocol for a given key.
|
||||
*/
|
||||
setToggleHangingProtocol: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
toggleHangingProtocol: {
|
||||
...state.toggleHangingProtocol,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setToggleHangingProtocol'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears all toggle hanging protocols.
|
||||
*/
|
||||
clearToggleHangingProtocol: () =>
|
||||
set({ toggleHangingProtocol: {} }, false, 'clearToggleHangingProtocol'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing toggle hanging protocols.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useToggleHangingProtocolStore = create<ToggleHangingProtocolState>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createToggleHangingProtocolStore, { name: 'ToggleHangingProtocolStore' })
|
||||
: createToggleHangingProtocolStore
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'toggleOneUpViewportGridId';
|
||||
|
||||
type ToggleOneUpViewportGridState = {
|
||||
toggleOneUpViewportGridStore: any | null;
|
||||
setToggleOneUpViewportGridStore: (state: any) => void;
|
||||
clearToggleOneUpViewportGridStore: () => void;
|
||||
type: string;
|
||||
};
|
||||
|
||||
// Stores the entire ViewportGridService getState when toggling to one up
|
||||
// (e.g. via a double click) so that it can be restored when toggling back.
|
||||
export const useToggleOneUpViewportGridStore = create<ToggleOneUpViewportGridState>(set => ({
|
||||
toggleOneUpViewportGridStore: null,
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
setToggleOneUpViewportGridStore: state => set({ toggleOneUpViewportGridStore: state }),
|
||||
clearToggleOneUpViewportGridStore: () => set({ toggleOneUpViewportGridStore: null }),
|
||||
}));
|
||||
@@ -0,0 +1,87 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
/**
|
||||
* Identifier for the UI State store type.
|
||||
*/
|
||||
const PRESENTATION_TYPE_ID = 'uiStateId';
|
||||
|
||||
/**
|
||||
* Flag to enable or disable debug mode for the store.
|
||||
* Set to `true` to enable zustand devtools.
|
||||
*/
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the UI state.
|
||||
*/
|
||||
type UIState = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* State shape for the UI State store.
|
||||
*/
|
||||
type UIStateStore = {
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Stores the UI state as a key-value mapping.
|
||||
*/
|
||||
uiState: UIState;
|
||||
|
||||
/**
|
||||
* Sets the UI state for a given key.
|
||||
*
|
||||
* @param key - The key to set in the UI state.
|
||||
* @param value - The value to associate with the key.
|
||||
*/
|
||||
setUIState: (key: string, value: unknown) => void;
|
||||
|
||||
/**
|
||||
* Clears all UI state.
|
||||
*/
|
||||
clearUIState: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the UI State store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The UI State store state and actions.
|
||||
*/
|
||||
const createUIStateStore = (set): UIStateStore => ({
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
uiState: {},
|
||||
|
||||
/**
|
||||
* Sets the UI state for a given key.
|
||||
*/
|
||||
setUIState: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
uiState: {
|
||||
...state.uiState,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setUIState'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears all UI state.
|
||||
*/
|
||||
clearUIState: () => set({ uiState: {} }, false, 'clearUIState'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing UI state.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useUIStateStore = create<UIStateStore>()(
|
||||
DEBUG_STORE ? devtools(createUIStateStore, { name: 'UIStateStore' }) : createUIStateStore
|
||||
);
|
||||
@@ -0,0 +1,89 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
/**
|
||||
* Identifier for the viewport grid store type.
|
||||
*/
|
||||
const PRESENTATION_TYPE_ID = 'viewportGridId';
|
||||
|
||||
/**
|
||||
* Flag to enable or disable debug mode for the store.
|
||||
* Set to `true` to enable zustand devtools.
|
||||
*/
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state of the viewport grid.
|
||||
*/
|
||||
type ViewportGridState = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* State shape for the Viewport Grid store.
|
||||
*/
|
||||
type ViewportGridStore = {
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Stores the viewport grid state as a key-value mapping.
|
||||
*/
|
||||
viewportGridState: ViewportGridState;
|
||||
|
||||
/**
|
||||
* Sets the viewport grid state for a given key.
|
||||
*
|
||||
* @param key - The key to set in the viewport grid state.
|
||||
* @param value - The value to associate with the key.
|
||||
*/
|
||||
setViewportGridState: (key: string, value: unknown) => void;
|
||||
|
||||
/**
|
||||
* Clears the entire viewport grid state.
|
||||
*/
|
||||
clearViewportGridState: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Viewport Grid store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The Viewport Grid store state and actions.
|
||||
*/
|
||||
const createViewportGridStore = (set): ViewportGridStore => ({
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
viewportGridState: {},
|
||||
|
||||
/**
|
||||
* Sets the viewport grid state for a given key.
|
||||
*/
|
||||
setViewportGridState: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
viewportGridState: {
|
||||
...state.viewportGridState,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setViewportGridState'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears the entire viewport grid state.
|
||||
*/
|
||||
clearViewportGridState: () => set({ viewportGridState: {} }, false, 'clearViewportGridState'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing viewport grid state.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useViewportGridStore = create<ViewportGridStore>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createViewportGridStore, { name: 'ViewportGridStore' })
|
||||
: createViewportGridStore
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'viewportsByPositionId';
|
||||
const DEBUG_STORE = true;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing viewports by position.
|
||||
*/
|
||||
type ViewportsByPositionState = {
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Stores viewports indexed by their position.
|
||||
*/
|
||||
viewportsByPosition: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Stores initial display viewports as an array of strings.
|
||||
*/
|
||||
initialInDisplay: string[];
|
||||
|
||||
/**
|
||||
* Sets the viewport for a given key.
|
||||
*
|
||||
* @param key - The key identifying the viewport position.
|
||||
* @param value - The viewport data to associate with the key.
|
||||
*/
|
||||
setViewportsByPosition: (key: string, value: unknown) => void;
|
||||
|
||||
/**
|
||||
* Clears all viewports by position.
|
||||
*/
|
||||
clearViewportsByPosition: () => void;
|
||||
|
||||
/**
|
||||
* Adds an initial display viewport.
|
||||
*
|
||||
* @param value - The viewport identifier to add.
|
||||
*/
|
||||
addInitialInDisplay: (value: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Viewports By Position store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The Viewports By Position store state and actions.
|
||||
*/
|
||||
const createViewportsByPositionStore = (set): ViewportsByPositionState => ({
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
viewportsByPosition: {},
|
||||
initialInDisplay: [],
|
||||
|
||||
/**
|
||||
* Sets the viewport for a given key.
|
||||
*/
|
||||
setViewportsByPosition: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
viewportsByPosition: {
|
||||
...state.viewportsByPosition,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setViewportsByPosition'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears all viewports by position.
|
||||
*/
|
||||
clearViewportsByPosition: () =>
|
||||
set({ viewportsByPosition: {} }, false, 'clearViewportsByPosition'),
|
||||
|
||||
/**
|
||||
* Adds an initial display viewport.
|
||||
*/
|
||||
addInitialInDisplay: value =>
|
||||
set(
|
||||
state => ({
|
||||
initialInDisplay: [...state.initialInDisplay, value],
|
||||
}),
|
||||
false,
|
||||
'addInitialInDisplay'
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing viewports by position.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useViewportsByPositionStore = create<ViewportsByPositionState>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createViewportsByPositionStore, { name: 'ViewportsByPositionStore' })
|
||||
: createViewportsByPositionStore
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
};
|
||||
|
||||
export default RESPONSE;
|
||||
@@ -0,0 +1,10 @@
|
||||
const MIN_SR_SERIES_NUMBER = 4700;
|
||||
|
||||
export default function getNextSRSeriesNumber(displaySetService) {
|
||||
const activeDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
|
||||
const srSeriesNumbers = srDisplaySets.map(ds => ds.SeriesNumber);
|
||||
const maxSeriesNumber = Math.max(...srSeriesNumbers, MIN_SR_SERIES_NUMBER);
|
||||
|
||||
return maxSeriesNumber + 1;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {*} data.text
|
||||
* @param {*} data.label
|
||||
* @param {*} event
|
||||
* @param {*} callback
|
||||
* @param {*} isArrowAnnotateInputDialog
|
||||
* @param {*} dialogConfig
|
||||
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
|
||||
* @param {string?} dialogConfig.inputLabel - show label above the input
|
||||
*/
|
||||
|
||||
export function callInputDialog(
|
||||
uiDialogService,
|
||||
data,
|
||||
callback,
|
||||
isArrowAnnotateInputDialog = true,
|
||||
dialogConfig: any = {}
|
||||
) {
|
||||
const dialogId = 'dialog-enter-annotation';
|
||||
const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : '';
|
||||
const {
|
||||
dialogTitle = 'Annotation',
|
||||
inputLabel = 'Enter your annotation',
|
||||
validateFunc = value => true,
|
||||
} = dialogConfig;
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(value.label, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: dialogTitle,
|
||||
value: { label },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
return (
|
||||
<Input
|
||||
autoFocus
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
id="annotation"
|
||||
label={inputLabel}
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
value={value.label}
|
||||
onChange={event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
}}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function callLabelAutocompleteDialog(uiDialogService, callback, dialogConfig, labelConfig) {
|
||||
const exclusive = labelConfig ? labelConfig.exclusive : false;
|
||||
const dropDownItems = labelConfig ? labelConfig.items : [];
|
||||
|
||||
const { validateFunc = value => true } = dialogConfig;
|
||||
|
||||
const labellingDoneCallback = value => {
|
||||
if (typeof value === 'string') {
|
||||
if (typeof validateFunc === 'function' && !validateFunc(value)) {
|
||||
return;
|
||||
}
|
||||
callback(value, 'save');
|
||||
} else {
|
||||
callback('', 'cancel');
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'select-annotation' });
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'select-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: LabellingFlow,
|
||||
contentProps: {
|
||||
labellingDoneCallback: labellingDoneCallback,
|
||||
measurementData: { label: '' },
|
||||
componentClassName: {},
|
||||
labelData: dropDownItems,
|
||||
exclusive: exclusive,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig) {
|
||||
const exclusive = labelConfig ? labelConfig.exclusive : false;
|
||||
const dropDownItems = labelConfig ? labelConfig.items : [];
|
||||
return new Promise<Map<any, any>>((resolve, reject) => {
|
||||
const labellingDoneCallback = value => {
|
||||
uiDialogService.dismiss({ id: 'select-annotation' });
|
||||
if (typeof value === 'string') {
|
||||
measurement.label = value;
|
||||
}
|
||||
resolve(measurement);
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'select-annotation',
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: LabellingFlow,
|
||||
defaultPosition: {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
},
|
||||
contentProps: {
|
||||
labellingDoneCallback: labellingDoneCallback,
|
||||
measurementData: measurement,
|
||||
componentClassName: {},
|
||||
labelData: dropDownItems,
|
||||
exclusive: exclusive,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default callInputDialog;
|
||||
@@ -0,0 +1,3 @@
|
||||
.chrome-picker {
|
||||
background: #090c29 !important;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Dialog } from '@ohif/ui';
|
||||
import { ChromePicker } from 'react-color';
|
||||
|
||||
import './colorPickerDialog.css';
|
||||
|
||||
function colorPickerDialog(uiDialogService, rgbaColor, callback) {
|
||||
const dialogId = 'pick-color';
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.rgbaColor, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment Color',
|
||||
value: { rgbaColor },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
const handleChange = color => {
|
||||
setValue({ rgbaColor: color.rgb });
|
||||
};
|
||||
|
||||
return (
|
||||
<ChromePicker
|
||||
color={value.rgbaColor}
|
||||
onChange={handleChange}
|
||||
presetColors={[]}
|
||||
width={300}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default colorPickerDialog;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { showLabelAnnotationPopup } from './callInputDialog';
|
||||
|
||||
function promptLabelAnnotation({ servicesManager }, ctx, evt) {
|
||||
const { measurementService, customizationService } = servicesManager.services;
|
||||
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt;
|
||||
return new Promise(async function (resolve) {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(measurementId);
|
||||
const value = await showLabelAnnotationPopup(
|
||||
measurement,
|
||||
servicesManager.services.uiDialogService,
|
||||
labelConfig
|
||||
);
|
||||
|
||||
measurementService.update(
|
||||
measurementId,
|
||||
{
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
resolve({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default promptLabelAnnotation;
|
||||
@@ -0,0 +1,75 @@
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import { createReportDialogPrompt } from '../Panels';
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
|
||||
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
|
||||
const { uiDialogService, measurementService, displaySetService } = servicesManager.services;
|
||||
const viewportId = evt.viewportId === undefined ? evt.data.viewportId : evt.viewportId;
|
||||
const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
|
||||
const StudyInstanceUID = evt?.data?.StudyInstanceUID;
|
||||
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
|
||||
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
try {
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = measurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements
|
||||
.filter(
|
||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
||||
)
|
||||
.filter(m => m.referencedImageId != null);
|
||||
|
||||
const SeriesDescription =
|
||||
// isUndefinedOrEmpty
|
||||
promptResult.value === undefined || promptResult.value === ''
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: trackedMeasurements,
|
||||
dataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
};
|
||||
displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport,
|
||||
});
|
||||
} else if (promptResult.action === RESPONSE.CANCEL) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return {
|
||||
userResponse: promptResult.action,
|
||||
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
isBackupSave,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default promptSaveReport;
|
||||
@@ -1,4 +1,7 @@
|
||||
import { HangingProtocolService, StateSyncService, Types } from '@ohif/core';
|
||||
import { HangingProtocolService, Types } from '@ohif/core';
|
||||
import { useViewportGridStore } from '../stores/useViewportGridStore';
|
||||
import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore';
|
||||
import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore';
|
||||
|
||||
export type ReturnType = {
|
||||
hangingProtocolStageIndexMap: Record<string, Types.HangingProtocol.HPInfo>;
|
||||
@@ -14,11 +17,7 @@ export type ReturnType = {
|
||||
* @returns Set of states that can be applied to the state sync to remember
|
||||
* the current view state.
|
||||
*/
|
||||
const reuseCachedLayout = (
|
||||
state,
|
||||
hangingProtocolService: HangingProtocolService,
|
||||
syncService: StateSyncService
|
||||
): ReturnType => {
|
||||
const reuseCachedLayout = (state, hangingProtocolService: HangingProtocolService): ReturnType => {
|
||||
const { activeViewportId } = state;
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
|
||||
@@ -29,16 +28,14 @@ const reuseCachedLayout = (
|
||||
const hpInfo = hangingProtocolService.getState();
|
||||
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
|
||||
|
||||
const syncState = syncService.getState();
|
||||
const viewportGridStore = { ...syncState.viewportGridStore };
|
||||
const displaySetSelectorMap = { ...syncState.displaySetSelectorMap };
|
||||
const { viewportGridState, setViewportGridState } = useViewportGridStore.getState();
|
||||
const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
||||
const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } =
|
||||
useHangingProtocolStageIndexStore.getState();
|
||||
|
||||
const stage = protocol.stages[stageIndex];
|
||||
const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`;
|
||||
const cacheId = `${activeStudyUID}:${protocolId}`;
|
||||
const hangingProtocolStageIndexMap = {
|
||||
...syncState.hangingProtocolStageIndexMap,
|
||||
};
|
||||
const { rows, columns } = stage.viewportStructure.properties;
|
||||
const custom =
|
||||
stage.viewports.length !== state.viewports.size ||
|
||||
@@ -48,7 +45,7 @@ const reuseCachedLayout = (
|
||||
hangingProtocolStageIndexMap[cacheId] = hpInfo;
|
||||
|
||||
if (storeId && custom) {
|
||||
viewportGridStore[storeId] = { ...state };
|
||||
setViewportGridState(storeId, { ...state });
|
||||
}
|
||||
|
||||
state.viewports.forEach((viewport, viewportId) => {
|
||||
@@ -62,21 +59,24 @@ const reuseCachedLayout = (
|
||||
continue;
|
||||
}
|
||||
if (viewportId === activeViewportId && i === 0) {
|
||||
displaySetSelectorMap[`${activeStudyUID}:activeDisplaySet:0`] = displaySetUID;
|
||||
setDisplaySetSelector(`${activeStudyUID}:activeDisplaySet:0`, displaySetUID);
|
||||
}
|
||||
if (displaySetOptions[i]?.id) {
|
||||
displaySetSelectorMap[
|
||||
setDisplaySetSelector(
|
||||
`${activeStudyUID}:${displaySetOptions[i].id}:${
|
||||
displaySetOptions[i].matchedDisplaySetsIndex || 0
|
||||
}`
|
||||
] = displaySetUID;
|
||||
}`,
|
||||
displaySetUID
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setHangingProtocolStageIndex(cacheId, hpInfo);
|
||||
|
||||
return {
|
||||
hangingProtocolStageIndexMap,
|
||||
viewportGridStore,
|
||||
viewportGridStore: viewportGridState,
|
||||
displaySetSelectorMap,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user