Initial commit from prod-batam
This commit is contained in:
10
extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts
Normal file
10
extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
8
extensions/default/src/utils/addIcon.ts
Normal file
8
extensions/default/src/utils/addIcon.ts
Normal 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);
|
||||
}
|
||||
20
extensions/default/src/utils/calculateScanAxisNormal.ts
Normal file
20
extensions/default/src/utils/calculateScanAxisNormal.ts
Normal 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);
|
||||
}
|
||||
158
extensions/default/src/utils/callInputDialog.tsx
Normal file
158
extensions/default/src/utils/callInputDialog.tsx
Normal 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;
|
||||
3
extensions/default/src/utils/colorPickerDialog.css
Normal file
3
extensions/default/src/utils/colorPickerDialog.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.chrome-picker {
|
||||
background: #090c29 !important;
|
||||
}
|
||||
58
extensions/default/src/utils/colorPickerDialog.tsx
Normal file
58
extensions/default/src/utils/colorPickerDialog.tsx
Normal 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;
|
||||
32
extensions/default/src/utils/createRenderedRetrieve.js
Normal file
32
extensions/default/src/utils/createRenderedRetrieve.js
Normal 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;
|
||||
46
extensions/default/src/utils/createRenderedRetrieve.test.js
Normal file
46
extensions/default/src/utils/createRenderedRetrieve.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
45
extensions/default/src/utils/getBulkdataValue.js
Normal file
45
extensions/default/src/utils/getBulkdataValue.js
Normal 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;
|
||||
105
extensions/default/src/utils/getBulkdataValue.test.js
Normal file
105
extensions/default/src/utils/getBulkdataValue.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
169
extensions/default/src/utils/getDirectURL.test.js
Normal file
169
extensions/default/src/utils/getDirectURL.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
65
extensions/default/src/utils/getDirectURL.ts
Normal file
65
extensions/default/src/utils/getDirectURL.ts
Normal 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;
|
||||
10
extensions/default/src/utils/getNextSRSeriesNumber.js
Normal file
10
extensions/default/src/utils/getNextSRSeriesNumber.js
Normal 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;
|
||||
}
|
||||
1
extensions/default/src/utils/index.ts
Normal file
1
extensions/default/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { addIcon } from './addIcon';
|
||||
31
extensions/default/src/utils/promptLabelAnnotation.js
Normal file
31
extensions/default/src/utils/promptLabelAnnotation.js
Normal 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;
|
||||
75
extensions/default/src/utils/promptSaveReport.js
Normal file
75
extensions/default/src/utils/promptSaveReport.js
Normal 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;
|
||||
84
extensions/default/src/utils/reuseCachedLayouts.ts
Normal file
84
extensions/default/src/utils/reuseCachedLayouts.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
25
extensions/default/src/utils/validations/checkMultiframe.ts
Normal file
25
extensions/default/src/utils/validations/checkMultiframe.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user