Initial commit from prod-batam

This commit is contained in:
mario
2025-05-27 10:51:12 +07:00
commit e0befad0b8
3361 changed files with 304290 additions and 0 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
import { addIcon as addIconUI } from '@ohif/ui';
import { Icons } from '@ohif/ui-next';
/** Adds the icon to both ui and ui-next */
export function addIcon(name, icon) {
addIconUI(name, icon);
Icons.addIcon(name, icon);
}

View File

@@ -0,0 +1,20 @@
import { vec3 } from 'gl-matrix';
/**
* Calculates the scanAxisNormal based on a image orientation vector extract from a frame
* @param {*} imageOrientation
* @returns
*/
export default function calculateScanAxisNormal(imageOrientation) {
const rowCosineVec = vec3.fromValues(
imageOrientation[0],
imageOrientation[1],
imageOrientation[2]
);
const colCosineVec = vec3.fromValues(
imageOrientation[3],
imageOrientation[4],
imageOrientation[5]
);
return vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
}

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
.chrome-picker {
background: #090c29 !important;
}

View File

@@ -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;

View File

@@ -0,0 +1,32 @@
/**
* Generates the rendered URL that can be used for direct retrieve of the pixel data binary stream.
*
* @param {object} config - The configuration object.
* @param {string} config.wadoRoot - The root URL for the WADO service.
* @param {object} params - The parameters object.
* @param {string} params.tag - The tag name of the URL to retrieve.
* @param {string} params.defaultPath - The path for the pixel data URL.
* @param {object} params.instance - The instance object that the tag is in.
* @param {string} params.defaultType - The mime type of the response.
* @param {string} params.singlepart - The type of the part to retrieve.
* @param {string} params.fetchPart - Unknown parameter.
* @param {string} params.url - Unknown parameter.
* @returns {string|Promise<string>} - An absolute URL to the binary stream.
*/
const createRenderedRetrieve = (config, params) => {
const { wadoRoot } = config;
const { instance, tag = 'PixelData' } = params;
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const bulkDataURI = instance[tag]?.BulkDataURI ?? '';
if (bulkDataURI?.indexOf('?') !== -1) {
// The value instance has parameters, so it should not revert to the rendered
return;
}
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
}
};
export default createRenderedRetrieve;

View File

@@ -0,0 +1,46 @@
import createRenderedRetrieve from './createRenderedRetrieve';
describe('createRenderedRetrieve', () => {
const config = {
wadoRoot: 'https://example.com/wado',
};
const params = {
instance: {
StudyInstanceUID: 'study-uid',
SeriesInstanceUID: 'series-uid',
SOPInstanceUID: 'sop-uid',
},
};
it('should return the rendered URL for PixelData tag', () => {
const result = createRenderedRetrieve(config, {
...params,
tag: 'PixelData',
});
expect(result).toBe(
'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered'
);
});
it('should return the rendered URL for EncapsulatedDocument tag', () => {
const result = createRenderedRetrieve(config, {
...params,
tag: 'EncapsulatedDocument',
});
expect(result).toBe(
'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered'
);
});
it('should return undefined for unknown tag', () => {
const result = createRenderedRetrieve(config, {
...params,
tag: 'UnknownTag',
});
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,41 @@
import { DisplaySetService, Types } from '@ohif/core';
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
/**
* Find an SR having the same series description.
* This is used by the store service in order to store DICOM SR's having the
* same Series Description into a single series under consecutive instance numbers
* That way, they are all organized as a set and could have tools to view
* "prior" SR instances.
*
* @param SeriesDescription - is the description to look for
* @param displaySetService - the display sets to search for DICOM SR in
* @returns SeriesMetadata from a DICOM SR having the same series description
*/
export default function findSRWithSameSeriesDescription(
SeriesDescription: string,
displaySetService: DisplaySetService
): Types.SeriesMetadata {
const activeDisplaySets = displaySetService.getActiveDisplaySets();
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
const sameSeries = srDisplaySets.find(ds => ds.SeriesDescription === SeriesDescription);
if (sameSeries) {
console.log('Storing to same series', sameSeries);
const { instance } = sameSeries;
const { SeriesInstanceUID, SeriesDescription, SeriesDate, SeriesTime, SeriesNumber, Modality } =
instance;
return {
SeriesInstanceUID,
SeriesDescription,
SeriesDate,
SeriesTime,
SeriesNumber,
Modality,
InstanceNumber: sameSeries.instances.length + 1,
};
}
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
return { SeriesDescription, SeriesNumber };
}

View File

@@ -0,0 +1,45 @@
/**
* Generates a URL that can be used for direct retrieve of the bulkdata.
*
* @param {object} config - The configuration object.
* @param {object} params - The parameters object.
* @param {string} params.tag - The tag name of the URL to retrieve.
* @param {string} params.defaultPath - The path for the pixel data URL.
* @param {object} params.instance - The instance object that the tag is in.
* @param {string} params.defaultType - The mime type of the response.
* @param {string} params.singlepart - The type of the part to retrieve.
* @param {string} params.fetchPart - Unknown.
* @returns {string|Promise<string>} - An absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI.
*/
const getBulkdataValue = (config, params) => {
const {
instance,
tag = 'PixelData',
defaultPath = '/pixeldata',
defaultType = 'video/mp4',
} = params;
const value = instance[tag];
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const BulkDataURI =
(value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
const hasQuery = BulkDataURI.indexOf('?') !== -1;
const hasAccept = BulkDataURI.indexOf('accept=') !== -1;
const acceptUri =
BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
if (acceptUri.startsWith('series/')) {
const { wadoRoot } = config;
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
}
// The DICOMweb standard states that the default is multipart related, and then
// separately states that the accept parameter is the URL parameter equivalent of the accept header.
return acceptUri;
};
export default getBulkdataValue;

View File

@@ -0,0 +1,105 @@
import getBulkdataValue from './getBulkdataValue';
jest.mock('@ohif/core');
global.URL.createObjectURL = jest.fn(() => 'blob:');
describe('getBulkdataValue', () => {
const config = {
singlepart: true,
};
const params = {
instance: {
StudyInstanceUID: 'study-uid',
SeriesInstanceUID: 'series-uid',
SOPInstanceUID: 'sop-uid',
},
};
it('should return the BulkDataURI with defaultType if singlepart is true without accept', () => {
const value = {
BulkDataURI: 'https://example.com/bulkdata',
retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])),
};
const result = getBulkdataValue(config, {
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toContain(value.BulkDataURI);
expect(result).toContain('accept=video/mp4');
const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0;
expect(acceptCount).toBe(1);
});
it('should return the BulkDataURI with accept', () => {
const value = {
BulkDataURI: 'https://example.com/bulkdata?accept=video/mp4',
};
const result = getBulkdataValue(config, {
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toContain(value.BulkDataURI);
expect(result).toContain('accept=video/mp4');
const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0;
expect(acceptCount).toBe(1);
});
it('should return the BulkDataURI with accept and query params', () => {
const value = {
BulkDataURI: 'https://example.com/bulkdata?test=123',
};
const result = getBulkdataValue(config, {
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toContain(value.BulkDataURI);
expect(result).toContain('accept=video/mp4');
expect(result).toContain('test=123');
const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0;
expect(acceptCount).toBe(1);
});
it('should return default path with accept', () => {
const value = {
BulkDataURI: null,
};
const defaultPath = '/testing';
const defaultURI = `series/${params.instance.SeriesInstanceUID}/instances/${params.instance.SOPInstanceUID}${defaultPath}`;
const result = getBulkdataValue(config, {
...params,
defaultPath,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toContain(defaultURI);
expect(result).toContain('accept=video/mp4');
const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0;
expect(acceptCount).toBe(1);
});
});

View File

@@ -0,0 +1,169 @@
import getDirectURL from './getDirectURL';
import getBulkdataValue from './getBulkdataValue';
import createRenderedRetrieve from './createRenderedRetrieve';
jest.mock('@ohif/core');
jest.mock('./getBulkdataValue');
jest.mock('./createRenderedRetrieve');
global.URL.createObjectURL = jest.fn(() => 'blob:');
describe('getDirectURL', () => {
const config = {
singlepart: true,
defaultType: 'video/mp4',
};
const params = {
tag: 'PixelData',
defaultPath: '/path/to/pixeldata',
instance: {
StudyInstanceUID: 'study-uid',
SeriesInstanceUID: 'series-uid',
SOPInstanceUID: 'sop-uid',
},
};
it('should return the provided URL if it exists', () => {
const url = 'https://example.com/direct-retrieve';
const result = getDirectURL(config, {
...params,
url: 'https://example.com/direct-retrieve',
});
expect(result).toBe(url);
});
it('should return the DirectRetrieveURL if it exists', () => {
const value = {
DirectRetrieveURL: 'https://example.com/direct-retrieve',
};
const result = getDirectURL(config, {
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toBe(value.DirectRetrieveURL);
});
it('should return the URL for InlineBinary', () => {
const value = {
InlineBinary: 'base64-encoded-data',
};
const result = getDirectURL(config, {
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
});
expect(result).toContain('blob:');
});
it('should return the BulkDataURI with defaultType if singlepart is false and there is no retrieveBulkData', () => {
const value = {
BulkDataURI: 'https://example.com/bulkdata',
};
const result = getDirectURL(
{
...config,
singlepart: false,
},
{
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
}
);
expect(result).toBeUndefined();
});
it('should return the BulkDataURI with defaultType if singlepart is false with retrieveBulkData', async () => {
const value = {
BulkDataURI: 'https://example.com/bulkdata',
retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])),
};
const result = await getDirectURL(
{
...config,
singlepart: false,
},
{
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
}
);
expect(result).toContain('blob:');
});
it('should return the BulkDataURI with defaultType if singlepart does not include fetchPart', async () => {
const arr = new Uint8Array([0, 1, 2]);
const value = {
BulkDataURI: 'https://example.com/bulkdata',
retrieveBulkData: jest.fn().mockResolvedValueOnce(arr),
};
const result = await getDirectURL(
{
...config,
singlepart: ['audio'],
},
{
...params,
tag: 'PixelData',
instance: {
...params.instance,
PixelData: value,
},
}
);
expect(result).toContain('blob:');
expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([arr], { type: 'accept=video/mp4' }));
});
it('should return the URL from getBulkdataValue if it exists', () => {
const bulkDataURL = 'https://example.com/bulkdata';
getBulkdataValue.mockReturnValueOnce(bulkDataURL);
const result = getDirectURL(config, params);
expect(getBulkdataValue).toHaveBeenCalledWith(config, params);
expect(result).toBe(bulkDataURL);
});
it('should return the URL from createRenderedRetrieve if getBulkdataValue returns falsy', () => {
const renderedRetrieveURL = 'https://example.com/rendered-retrieve';
getBulkdataValue.mockReturnValueOnce(null);
createRenderedRetrieve.mockReturnValueOnce(renderedRetrieveURL);
const result = getDirectURL(config, params);
expect(getBulkdataValue).toHaveBeenCalledWith(config, params);
expect(createRenderedRetrieve).toHaveBeenCalledWith(config, params);
expect(result).toBe(renderedRetrieveURL);
});
});

View File

@@ -0,0 +1,65 @@
import { utils } from '@ohif/core';
import getBulkdataValue from './getBulkdataValue';
import createRenderedRetrieve from './createRenderedRetrieve';
/**
* Generates a URL that can be used for direct retrieve of the bulkdata
*
* @param {object} params
* @param {string} params.tag is the tag name of the URL to retrieve
* @param {string} params.defaultPath path for the pixel data url
* @param {object} params.instance is the instance object that the tag is in
* @param {string} params.defaultType is the mime type of the response
* @param {string} params.singlepart is the type of the part to retrieve
* @param {string} params.fetchPart unknown?
* @param {string} params.url unknown?
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
*/
const getDirectURL = (config, params) => {
const { singlepart } = config;
const {
instance,
tag = 'PixelData',
defaultType = 'video/mp4',
singlepart: fetchPart = 'video',
url = null,
} = params;
if (url) {
return url;
}
const value = instance[tag];
if (value) {
if (value.DirectRetrieveURL) {
return value.DirectRetrieveURL;
}
if (value.InlineBinary) {
const blob = utils.b64toBlob(value.InlineBinary, defaultType);
value.DirectRetrieveURL = URL.createObjectURL(blob);
return value.DirectRetrieveURL;
}
if (!singlepart || (singlepart !== true && singlepart.indexOf(fetchPart) === -1)) {
if (value.retrieveBulkData) {
// Try the specified retrieve type.
const options = {
mediaType: defaultType,
};
return value.retrieveBulkData(options).then(arr => {
value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType }));
return value.DirectRetrieveURL;
});
}
console.warn('Unable to retrieve', tag, 'from', instance);
return undefined;
}
}
return createRenderedRetrieve(config, params) || getBulkdataValue(config, params);
};
export default getDirectURL;

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
export { addIcon } from './addIcon';

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,84 @@
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>;
viewportGridStore: Record<string, unknown>;
displaySetSelectorMap: Record<string, string>;
};
/**
* Calculates a set of state information for hanging protocols and viewport grid
* which defines the currently applied hanging protocol state.
* @param state is the viewport grid state
* @param syncService is the state sync service to use for getting existing state
* @returns Set of states that can be applied to the state sync to remember
* the current view state.
*/
const reuseCachedLayout = (state, hangingProtocolService: HangingProtocolService): ReturnType => {
const { activeViewportId } = state;
const { protocol } = hangingProtocolService.getActiveProtocol();
if (!protocol) {
return;
}
const hpInfo = hangingProtocolService.getState();
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
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 { rows, columns } = stage.viewportStructure.properties;
const custom =
stage.viewports.length !== state.viewports.size ||
state.layout.numRows !== rows ||
state.layout.numCols !== columns;
hangingProtocolStageIndexMap[cacheId] = hpInfo;
if (storeId && custom) {
setViewportGridState(storeId, { ...state });
}
state.viewports.forEach((viewport, viewportId) => {
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
if (!displaySetOptions) {
return;
}
for (let i = 0; i < displaySetOptions.length; i++) {
const displaySetUID = displaySetInstanceUIDs[i];
if (!displaySetUID) {
continue;
}
if (viewportId === activeViewportId && i === 0) {
setDisplaySetSelector(`${activeStudyUID}:activeDisplaySet:0`, displaySetUID);
}
if (displaySetOptions[i]?.id) {
setDisplaySetSelector(
`${activeStudyUID}:${displaySetOptions[i].id}:${
displaySetOptions[i].matchedDisplaySetsIndex || 0
}`,
displaySetUID
);
}
}
});
setHangingProtocolStageIndex(cacheId, hpInfo);
return {
hangingProtocolStageIndexMap,
viewportGridStore: viewportGridState,
displaySetSelectorMap,
};
};
export default reuseCachedLayout;

View File

@@ -0,0 +1,24 @@
import toNumber from '@ohif/core/src/utils/toNumber';
/**
* Check if all voxels in series images has same number of components (samplesPerPixel)
* @param {*} instances
* @returns
*/
export default function areAllImageComponentsEqual(instances: Array<any>): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const { SamplesPerPixel } = instance;
if (SamplesPerPixel !== firstImageSamplesPerPixel) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,25 @@
import toNumber from '@ohif/core/src/utils/toNumber';
/**
* Check if the frames in a series has different dimensions
* @param {*} instances
* @returns
*/
export default function areAllImageDimensionsEqual(instances: Array<any>): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageRows = toNumber(firstImage.Rows);
const firstImageColumns = toNumber(firstImage.Columns);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const { Rows, Columns } = instance;
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,25 @@
import toNumber from '@ohif/core/src/utils/toNumber';
import { _isSameOrientation } from '@ohif/core/src/utils/isDisplaySetReconstructable';
/**
* Check is the series has frames with different orientations
* @param {*} instances
* @returns
*/
export default function areAllImageOrientationsEqual(instances: Array<any>): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageOrientationPatient = toNumber(firstImage.ImageOrientationPatient);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imageOrientationPatient = toNumber(instance.ImageOrientationPatient);
if (!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,69 @@
import { vec3 } from 'gl-matrix';
import toNumber from '@ohif/core/src/utils/toNumber';
import { _getPerpendicularDistance } from '@ohif/core/src/utils/isDisplaySetReconstructable';
import calculateScanAxisNormal from '../calculateScanAxisNormal';
/**
* Checks if there is a position shift between consecutive frames
* @param {*} previousPosition
* @param {*} actualPosition
* @param {*} scanAxisNormal
* @param {*} averageSpacingBetweenFrames
* @returns
*/
function _checkSeriesPositionShift(
previousPosition,
actualPosition,
scanAxisNormal,
averageSpacingBetweenFrames
) {
// predicted position should be the previous position added by the multiplication
// of the scanAxisNormal and the average spacing between frames
const predictedPosition = vec3.scaleAndAdd(
vec3.create(),
previousPosition,
scanAxisNormal,
averageSpacingBetweenFrames
);
return vec3.distance(actualPosition, predictedPosition) > averageSpacingBetweenFrames;
}
/**
* Checks if a series has position shifts between consecutive frames
* @param {*} instances
* @returns
*/
export default function areAllImagePositionsEqual(instances: Array<any>): boolean {
if (!instances?.length) {
return false;
}
const firstImageOrientationPatient = toNumber(instances[0].ImageOrientationPatient);
if (!firstImageOrientationPatient) {
return false;
}
const scanAxisNormal = calculateScanAxisNormal(firstImageOrientationPatient);
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
const lastIpp = toNumber(instances[instances.length - 1].ImagePositionPatient);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (instances.length - 1);
let previousImagePositionPatient = firstImagePositionPatient;
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
if (
_checkSeriesPositionShift(
previousImagePositionPatient,
imagePositionPatient,
scanAxisNormal,
averageSpacingBetweenFrames
)
) {
return false;
}
previousImagePositionPatient = imagePositionPatient;
}
return true;
}

View File

@@ -0,0 +1,64 @@
import {
_getPerpendicularDistance,
_getSpacingIssue,
reconstructionIssues,
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { DisplaySetMessage } from '@ohif/core';
import toNumber from '@ohif/core/src/utils/toNumber';
import { DisplaySetMessageList } from '@ohif/core';
/**
* Checks if series has spacing issues
* @param {*} instances
* @param {*} warnings
*/
export default function areAllImageSpacingEqual(
instances: Array<any>,
messages: DisplaySetMessageList
): void {
if (!instances?.length) {
return;
}
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
if (!firstImagePositionPatient) {
return;
}
const lastIpp = toNumber(instances[instances.length - 1].ImagePositionPatient);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (instances.length - 1);
let previousImagePositionPatient = firstImagePositionPatient;
const issuesFound = [];
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
const spacingBetweenFrames = _getPerpendicularDistance(
imagePositionPatient,
previousImagePositionPatient
);
const spacingIssue = _getSpacingIssue(spacingBetweenFrames, averageSpacingBetweenFrames);
if (spacingIssue) {
const issue = spacingIssue.issue;
// avoid multiple warning of the same thing
if (!issuesFound.includes(issue)) {
issuesFound.push(issue);
if (issue === reconstructionIssues.MISSING_FRAMES) {
messages.addMessage(DisplaySetMessage.CODES.MISSING_FRAMES);
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
messages.addMessage(DisplaySetMessage.CODES.IRREGULAR_SPACING);
}
}
// we just want to find issues not how many
if (issuesFound.length > 1) {
break;
}
}
previousImagePositionPatient = imagePositionPatient;
}
}

View File

@@ -0,0 +1,25 @@
import {
hasPixelMeasurements,
hasOrientation,
hasPosition,
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
/**
* Check various multi frame issues. It calls OHIF core functions
* @param {*} multiFrameInstance
* @param {*} warnings
*/
export default function checkMultiFrame(multiFrameInstance, messages: DisplaySetMessageList): void {
if (!hasPixelMeasurements(multiFrameInstance)) {
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_PIXEL_MEASUREMENTS);
}
if (!hasOrientation(multiFrameInstance)) {
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_ORIENTATION);
}
if (!hasPosition(multiFrameInstance)) {
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_POSITION_INFORMATION);
}
}

View File

@@ -0,0 +1,35 @@
import areAllImageDimensionsEqual from './areAllImageDimensionsEqual';
import areAllImageComponentsEqual from './areAllImageComponentsEqual';
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
import areAllImagePositionsEqual from './areAllImagePositionsEqual';
import areAllImageSpacingEqual from './areAllImageSpacingEqual';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
/**
* Runs various checks in a single frame series
* @param {*} instances
* @param {*} warnings
*/
export default function checkSingleFrames(
instances: Array<any>,
messages: DisplaySetMessageList
): void {
if (instances.length > 2) {
if (!areAllImageDimensionsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_DIMENSIONS);
}
if (!areAllImageComponentsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_COMPONENTS);
}
if (!areAllImageOrientationsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_ORIENTATIONS);
}
if (!areAllImagePositionsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_POSITION_INFORMATION);
}
areAllImageSpacingEqual(instances, messages);
}
}