chore: Tag everything currently unused as legacy.
This commit is contained in:
parent
8392c1f97e
commit
414ebc6850
@ -2,13 +2,12 @@ import cornerstone from 'cornerstone-core';
|
|||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
//import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
||||||
import { getEnabledElement } from './state';
|
import { getEnabledElement } from './state';
|
||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
const scroll = cornerstoneTools.import('util/scroll');
|
||||||
|
|
||||||
const { studyMetadataManager } = OHIF.utils;
|
const { studyMetadataManager } = OHIF.utils;
|
||||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||||
const { ViewportGridService } = servicesManager.services;
|
const { ViewportGridService } = servicesManager.services;
|
||||||
@ -299,9 +298,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||||
cornerstone.updateImage(element);
|
cornerstone.updateImage(element);
|
||||||
},
|
},
|
||||||
setCornerstoneLayout: () => {
|
// setCornerstoneLayout: () => {
|
||||||
setCornerstoneLayout();
|
// setCornerstoneLayout();
|
||||||
},
|
// },
|
||||||
setWindowLevel: ({ window, level }) => {
|
setWindowLevel: ({ window, level }) => {
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
const enabledElement = _getActiveViewportsEnabledElement();
|
||||||
|
|
||||||
@ -430,12 +429,12 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { toolName: 'Zoom' },
|
options: { toolName: 'Zoom' },
|
||||||
},
|
},
|
||||||
setCornerstoneLayout: {
|
// setCornerstoneLayout: {
|
||||||
commandFn: actions.setCornerstoneLayout,
|
// commandFn: actions.setCornerstoneLayout,
|
||||||
storeContexts: [],
|
// storeContexts: [],
|
||||||
options: {},
|
// options: {},
|
||||||
context: 'VIEWER',
|
// context: 'VIEWER',
|
||||||
},
|
// },
|
||||||
setWindowLevel: {
|
setWindowLevel: {
|
||||||
commandFn: actions.setWindowLevel,
|
commandFn: actions.setWindowLevel,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
|
|||||||
@ -271,7 +271,7 @@ export default function init({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { csToolsConfig } = configuration;
|
const { csToolsConfig } = configuration;
|
||||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
|
|
||||||
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT']);
|
||||||
|
|
||||||
|
export default function isLowPriorityModality(Modality) {
|
||||||
|
return LOW_PRIORITY_MODALITIES.includes(Modality);
|
||||||
|
}
|
||||||
97
extensions/default/src/DicomWebDataSource/utils/sortStudy.js
Normal file
97
extensions/default/src/DicomWebDataSource/utils/sortStudy.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import isLowPriorityModality from './isLowPriorityModality';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Series sorting criteria: series considered low priority are moved to the end
|
||||||
|
* of the list and series number is used to break ties
|
||||||
|
* @param {Object} firstSeries
|
||||||
|
* @param {Object} secondSeries
|
||||||
|
*/
|
||||||
|
function seriesInfoSortingCriteria(firstSeries, secondSeries) {
|
||||||
|
const aLowPriority = isLowPriorityModality(firstSeries.Modality);
|
||||||
|
const bLowPriority = isLowPriorityModality(secondSeries.Modality);
|
||||||
|
if (!aLowPriority && bLowPriority) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aLowPriority && !bLowPriority) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSeries.SeriesNumber - secondSeries.SeriesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesSortCriteria = {
|
||||||
|
default: (a, b) => a.SeriesNumber - b.SeriesNumber,
|
||||||
|
seriesInfoSortingCriteria,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instancesSortCriteria = {
|
||||||
|
default: (a, b) => a.InstanceNumber - b.InstanceNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortingCriteria = {
|
||||||
|
seriesSortCriteria,
|
||||||
|
instancesSortCriteria,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts given series (given param is modified)
|
||||||
|
* The default criteria is based on series number in ascending order.
|
||||||
|
*
|
||||||
|
* @param {Array} series List of series
|
||||||
|
* @param {function} seriesSortingCriteria method for sorting
|
||||||
|
* @returns {Array} sorted series object
|
||||||
|
*/
|
||||||
|
const sortStudySeries = (
|
||||||
|
series,
|
||||||
|
seriesSortingCriteria = seriesSortCriteria.default
|
||||||
|
) => {
|
||||||
|
return series.sort(seriesSortingCriteria);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts given instancesList (given param is modified)
|
||||||
|
* The default criteria is based on instance number in ascending order.
|
||||||
|
*
|
||||||
|
* @param {Array} instancesList List of series
|
||||||
|
* @param {function} instancesSortingCriteria method for sorting
|
||||||
|
* @returns {Array} sorted instancesList object
|
||||||
|
*/
|
||||||
|
const sortStudyInstances = (
|
||||||
|
instancesList,
|
||||||
|
instancesSortingCriteria = instancesSortCriteria.default
|
||||||
|
) => {
|
||||||
|
return instancesList.sort(instancesSortingCriteria);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the series and instances (by default) inside a study instance based on sortingCriteria (given param is modified)
|
||||||
|
* The default criteria is based on series and instance numbers in ascending order.
|
||||||
|
*
|
||||||
|
* @param {Object} study The study instance
|
||||||
|
* @param {boolean} [deepSort = true] to sort instance also
|
||||||
|
* @param {function} [seriesSortingCriteria = seriesSortCriteria.default] method for sorting series
|
||||||
|
* @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances
|
||||||
|
* @returns {Object} sorted study object
|
||||||
|
*/
|
||||||
|
export default function sortStudy(
|
||||||
|
study,
|
||||||
|
deepSort = true,
|
||||||
|
seriesSortingCriteria = seriesSortCriteria.default,
|
||||||
|
instancesSortingCriteria = instancesSortCriteria.default
|
||||||
|
) {
|
||||||
|
if (!study || !study.series) {
|
||||||
|
throw new Error('Insufficient study data was provided to sortStudy');
|
||||||
|
}
|
||||||
|
|
||||||
|
sortStudySeries(study.series, seriesSortingCriteria);
|
||||||
|
|
||||||
|
if (deepSort) {
|
||||||
|
study.series.forEach(series => {
|
||||||
|
sortStudyInstances(series.instances, instancesSortingCriteria);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return study;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria };
|
||||||
@ -1,10 +1,7 @@
|
|||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import { studies } from '@ohif/core';
|
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
||||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||||
|
|
||||||
const { sortStudySeries, sortingCriteria } = studies;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||||
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import log from '../log';
|
import log from '../log';
|
||||||
import studies from '../studies';
|
// import studies from '../studies';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import {
|
// import {
|
||||||
retrieveMeasurementFromSR,
|
// retrieveMeasurementFromSR,
|
||||||
stowSRFromMeasurements,
|
// stowSRFromMeasurements,
|
||||||
} from './handleStructuredReport';
|
// } from './handleStructuredReport';
|
||||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import dcmjs from 'dcmjs';
|
|
||||||
import { api } from 'dicomweb-client';
|
|
||||||
|
|
||||||
import DICOMWeb from '../DICOMWeb';
|
|
||||||
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
|
||||||
import parseMeasurementsData from './parseMeasurementsData';
|
|
||||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
|
||||||
import errorHandler from '../errorHandler';
|
|
||||||
|
|
||||||
const VERSION_NAME = 'dcmjs-0.0';
|
|
||||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
|
|
||||||
*
|
|
||||||
* @param {Array} series - List of all series metaData loaded
|
|
||||||
* @param {Array} studies - List of all studies metaData loaded
|
|
||||||
* @param {string} serverUrl - Server URL to be used on request
|
|
||||||
* @returns {Object} MeasurementData
|
|
||||||
*/
|
|
||||||
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
|
||||||
const config = {
|
|
||||||
url: serverUrl,
|
|
||||||
headers: DICOMWeb.getAuthorizationHeader(),
|
|
||||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
|
||||||
|
|
||||||
const instance = series.getFirstInstance();
|
|
||||||
const options = {
|
|
||||||
studyInstanceUID: instance.getStudyInstanceUID(),
|
|
||||||
seriesInstanceUID: instance.getSeriesInstanceUID(),
|
|
||||||
sopInstanceUID: instance.getSOPInstanceUID(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
|
|
||||||
const displaySets = getAllDisplaySets(studies);
|
|
||||||
const measurementsData = parseDicomStructuredReport(
|
|
||||||
part10SRArrayBuffer,
|
|
||||||
displaySets
|
|
||||||
);
|
|
||||||
|
|
||||||
return measurementsData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to store measurements to DICOM Structured Reports in determined server
|
|
||||||
*
|
|
||||||
* @param {Object} measurements - OHIF measurementData object
|
|
||||||
* @param {string} serverUrl - Server URL to be used on request
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
|
||||||
const { dataset } = parseMeasurementsData(measurements);
|
|
||||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
|
||||||
const meta = {
|
|
||||||
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
|
|
||||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
|
||||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
|
||||||
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
|
|
||||||
ImplementationClassUID: DicomMetaDictionary.uid(),
|
|
||||||
ImplementationVersionName: VERSION_NAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
|
|
||||||
const dicomDict = new DicomDict(denaturalized);
|
|
||||||
|
|
||||||
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
|
|
||||||
|
|
||||||
const part10Buffer = dicomDict.write();
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
url: serverUrl,
|
|
||||||
headers: DICOMWeb.getAuthorizationHeader(),
|
|
||||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
|
||||||
const options = {
|
|
||||||
datasets: [part10Buffer],
|
|
||||||
};
|
|
||||||
|
|
||||||
await dicomWeb.storeInstances(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
import dcmjs from 'dcmjs';
|
|
||||||
|
|
||||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
|
|
||||||
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
|
|
||||||
* (e.g. measurement table)
|
|
||||||
*
|
|
||||||
* @param {ArrayBuffer} part10SRArrayBuffer
|
|
||||||
* @param {Array} displaySets
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
|
||||||
// Get the dicom data as an Object
|
|
||||||
|
|
||||||
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
|
||||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
|
||||||
dicomData.dict
|
|
||||||
);
|
|
||||||
|
|
||||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
|
||||||
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
|
||||||
dataset
|
|
||||||
);
|
|
||||||
const measurementData = {};
|
|
||||||
let measurementNumber = 0;
|
|
||||||
|
|
||||||
Object.keys(storedMeasurementByToolType).forEach(toolName => {
|
|
||||||
const measurements = storedMeasurementByToolType[toolName];
|
|
||||||
measurementData[toolName] = [];
|
|
||||||
|
|
||||||
measurements.forEach(measurement => {
|
|
||||||
const instanceMetadata = findInstanceMetadataBySopInstanceUID(
|
|
||||||
displaySets,
|
|
||||||
measurement.sopInstanceUid
|
|
||||||
);
|
|
||||||
|
|
||||||
const { _study: study, _series: series } = instanceMetadata;
|
|
||||||
const { StudyInstanceUID, PatientID } = study;
|
|
||||||
const { SeriesInstanceUID } = series;
|
|
||||||
/* TODO: Update frameIndex to imageIndex for measurements */
|
|
||||||
const { sopInstanceUid, frameIndex } = measurement;
|
|
||||||
|
|
||||||
const imagePath = getImagePath(
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
sopInstanceUid,
|
|
||||||
frameIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
const imageId = instanceMetadata.getImageId();
|
|
||||||
if (!imageId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: We need the currentTimepointID set into the viewer
|
|
||||||
const currentTimepointId = 'TimepointId';
|
|
||||||
|
|
||||||
const toolData = Object.assign({}, measurement, {
|
|
||||||
imageId,
|
|
||||||
imagePath,
|
|
||||||
SOPInstanceUID: sopInstanceUid,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
PatientID,
|
|
||||||
measurementNumber: ++measurementNumber,
|
|
||||||
timepointId: currentTimepointId,
|
|
||||||
toolType: toolName,
|
|
||||||
_id: imageId + measurementNumber,
|
|
||||||
});
|
|
||||||
|
|
||||||
measurementData[toolName].push(toolData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return measurementData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to create imagePath with all imageData related
|
|
||||||
*
|
|
||||||
* @param {string} StudyInstanceUID
|
|
||||||
* @param {string} SeriesInstanceUID
|
|
||||||
* @param {string} SOPInstanceUID
|
|
||||||
* @param {string} frameIndex
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getImagePath = (
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameIndex
|
|
||||||
) => {
|
|
||||||
return [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameIndex].join(
|
|
||||||
'_'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parseDicomStructuredReport;
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import dcmjs from 'dcmjs';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
|
|
||||||
import log from '../log';
|
|
||||||
import measurements from '../measurements';
|
|
||||||
import isToolSupported from './utils/isToolSupported';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
|
|
||||||
*
|
|
||||||
* @param {Object} measurementsData - OHIF measurementData object
|
|
||||||
* @returns {Object} Dataset: measurement report from dcmjs
|
|
||||||
*/
|
|
||||||
const parseMeasurementsData = measurementsData => {
|
|
||||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
|
||||||
const { getImageIdForImagePath } = measurements;
|
|
||||||
|
|
||||||
const toolState = {};
|
|
||||||
const unsupportedTools = [];
|
|
||||||
|
|
||||||
Object.keys(measurementsData).forEach(measurementType => {
|
|
||||||
const annotations = measurementsData[measurementType];
|
|
||||||
|
|
||||||
annotations.forEach(annotation => {
|
|
||||||
const { toolType, imagePath } = annotation;
|
|
||||||
|
|
||||||
if (isToolSupported(toolType)) {
|
|
||||||
const imageId = getImageIdForImagePath(imagePath);
|
|
||||||
toolState[imageId] = toolState[imageId] || {};
|
|
||||||
toolState[imageId][toolType] = toolState[imageId][toolType] || {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
toolState[imageId][toolType].data.push(annotation);
|
|
||||||
} else {
|
|
||||||
unsupportedTools.push(toolType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (unsupportedTools.length > 0) {
|
|
||||||
log.warn(
|
|
||||||
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const report = MeasurementReport.generateReport(
|
|
||||||
toolState,
|
|
||||||
cornerstone.metaData
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
dataset: report.dataset,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parseMeasurementsData;
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import { studyMetadataManager } from '../utils';
|
|
||||||
|
|
||||||
import OHIFError from './OHIFError';
|
|
||||||
import { StudyMetadata } from './metadata/StudyMetadata';
|
|
||||||
import { StudyMetadataSource } from './StudyMetadataSource.js';
|
|
||||||
import { retrieveStudyMetadata } from '../studies/retrieveStudyMetadata.js';
|
|
||||||
|
|
||||||
export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
|
||||||
/**
|
|
||||||
* Get study metadata for a study with given study InstanceUID
|
|
||||||
* @param server
|
|
||||||
* @param {String} studyInstanceUID Study InstanceUID
|
|
||||||
* @return {Promise} A Promise object
|
|
||||||
*/
|
|
||||||
getByInstanceUID(server, studyInstanceUID) {
|
|
||||||
return retrieveStudyMetadata(server, studyInstanceUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load study info (OHIF.viewer.Studies) and study metadata (OHIF.viewer.StudyMetadataList) for a given study.
|
|
||||||
* @param {StudyMetadata} study StudyMetadata object.
|
|
||||||
*/
|
|
||||||
loadStudy(study) {
|
|
||||||
if (!(study instanceof StudyMetadata)) {
|
|
||||||
throw new OHIFError(
|
|
||||||
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudyMetadata'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const studyInstanceUID = study.getStudyInstanceUID();
|
|
||||||
|
|
||||||
if (study instanceof StudyMetadata) {
|
|
||||||
const alreadyLoaded = OHIF.viewer.Studies.findBy({
|
|
||||||
StudyInstanceUID: studyInstanceUID,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!alreadyLoaded) {
|
|
||||||
OHIFStudyMetadataSource._updateStudyCollections(study);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(study);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getByInstanceUID(studyInstanceUID)
|
|
||||||
.then(studyInfo => {
|
|
||||||
// Create study metadata object
|
|
||||||
const studyMetadata = new StudyMetadata(
|
|
||||||
studyInfo,
|
|
||||||
studyInfo.StudyInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get Study display sets
|
|
||||||
const displaySets = studyMetadata.createDisplaySets();
|
|
||||||
|
|
||||||
// Set studyMetadata display sets
|
|
||||||
studyMetadata.setDisplaySets(displaySets);
|
|
||||||
|
|
||||||
OHIFStudyMetadataSource._updateStudyCollections(studyMetadata);
|
|
||||||
resolve(studyMetadata);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static methods
|
|
||||||
static _updateStudyCollections(studyMetadata) {
|
|
||||||
const studyInfo = studyMetadata.getData();
|
|
||||||
|
|
||||||
// Set some studyInfo properties
|
|
||||||
studyInfo.selected = true;
|
|
||||||
studyInfo.displaySets = studyMetadata.getDisplaySets();
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,471 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
|
||||||
import {
|
|
||||||
clearStudyLoadingProgress,
|
|
||||||
setStudyLoadingProgress,
|
|
||||||
} from '../redux/actions';
|
|
||||||
import StackManager from '../utils/StackManager';
|
|
||||||
|
|
||||||
class BaseLoadingListener {
|
|
||||||
constructor(stack, options = {}) {
|
|
||||||
this.id = BaseLoadingListener.getNewId();
|
|
||||||
this.stack = stack;
|
|
||||||
this.startListening();
|
|
||||||
this.statsItemsLimit = options.statsItemsLimit || 2;
|
|
||||||
this.stats = {
|
|
||||||
items: [],
|
|
||||||
total: 0,
|
|
||||||
elapsedTime: 0,
|
|
||||||
speed: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._setProgressData = options._setProgressData;
|
|
||||||
this._clearProgressById = options._clearProgressById;
|
|
||||||
|
|
||||||
// Register the start point to make it possible to calculate
|
|
||||||
// bytes/s or frames/s when the first byte or frame is received
|
|
||||||
this._addStatsData(0);
|
|
||||||
|
|
||||||
// Update the progress before starting the download
|
|
||||||
// to make it possible to update the UI
|
|
||||||
this._updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
_addStatsData(value) {
|
|
||||||
const date = new Date();
|
|
||||||
const stats = this.stats;
|
|
||||||
const items = stats.items;
|
|
||||||
const newItem = {
|
|
||||||
value,
|
|
||||||
date,
|
|
||||||
};
|
|
||||||
|
|
||||||
items.push(newItem);
|
|
||||||
stats.total += newItem.value;
|
|
||||||
|
|
||||||
// Remove items until it gets below the limit
|
|
||||||
while (items.length > this.statsItemsLimit) {
|
|
||||||
const item = items.shift();
|
|
||||||
stats.total -= item.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the elapsedTime (seconds) based on first and last
|
|
||||||
// elements and recalculate the speed (bytes/s or frames/s)
|
|
||||||
if (items.length > 1) {
|
|
||||||
const oldestItem = items[0];
|
|
||||||
stats.elapsedTime =
|
|
||||||
(newItem.date.getTime() - oldestItem.date.getTime()) / 1000;
|
|
||||||
stats.speed = (stats.total - oldestItem.value) / stats.elapsedTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getProgressId() {
|
|
||||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
|
||||||
return 'StackProgress:' + displaySetInstanceUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearProgress() {
|
|
||||||
const progressId = this._getProgressId();
|
|
||||||
this._clearProgressById(progressId);
|
|
||||||
}
|
|
||||||
|
|
||||||
startListening() {
|
|
||||||
throw new Error('`startListening` must be implemented by child classes');
|
|
||||||
}
|
|
||||||
|
|
||||||
stopListening() {
|
|
||||||
throw new Error('`stopListening` must be implemented by child classes');
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stopListening();
|
|
||||||
this._clearProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
static getNewId() {
|
|
||||||
const timeSlice = new Date()
|
|
||||||
.getTime()
|
|
||||||
.toString()
|
|
||||||
.slice(-8);
|
|
||||||
const randomNumber = parseInt(Math.random() * 1000000000);
|
|
||||||
|
|
||||||
return timeSlice.toString() + randomNumber.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DICOMFileLoadingListener extends BaseLoadingListener {
|
|
||||||
constructor(stack, options) {
|
|
||||||
super(stack, options);
|
|
||||||
this._dataSetUrl = this._getDataSetUrl(stack);
|
|
||||||
this._lastLoaded = 0;
|
|
||||||
|
|
||||||
// Check how many instances has already been download (cached)
|
|
||||||
this._checkCachedData();
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkCachedData() {
|
|
||||||
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(
|
|
||||||
this._dataSetUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dataSet) {
|
|
||||||
const dataSetLength = dataSet.byteArray.length;
|
|
||||||
|
|
||||||
this._updateProgress({
|
|
||||||
percentComplete: 100,
|
|
||||||
loaded: dataSetLength,
|
|
||||||
total: dataSetLength,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getImageLoadProgressEventName() {
|
|
||||||
// TODO: Add this event as a constant in Cornerstone
|
|
||||||
return 'cornerstoneimageloadprogress.' + this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
startListening() {
|
|
||||||
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
|
||||||
|
|
||||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
this.stopListening();
|
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
imageLoadProgressEventName,
|
|
||||||
this.imageLoadProgressEventHandle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopListening() {
|
|
||||||
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
imageLoadProgressEventName,
|
|
||||||
this.imageLoadProgressEventHandle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageLoadProgressEventHandler = e => {
|
|
||||||
const eventData = e.detail;
|
|
||||||
const dataSetUrl = this._convertImageIdToDataSetUrl(eventData.imageId);
|
|
||||||
const bytesDiff = eventData.loaded - this._lastLoaded;
|
|
||||||
|
|
||||||
if (!this._dataSetUrl === dataSetUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the bytes downloaded to the stats
|
|
||||||
this._addStatsData(bytesDiff);
|
|
||||||
|
|
||||||
// Update the download progress
|
|
||||||
this._updateProgress(eventData);
|
|
||||||
|
|
||||||
// Cache the last eventData.loaded value
|
|
||||||
this._lastLoaded = eventData.loaded;
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateProgress(eventData) {
|
|
||||||
const progressId = this._getProgressId();
|
|
||||||
eventData = eventData || {};
|
|
||||||
|
|
||||||
const progressData = {
|
|
||||||
multiFrame: false,
|
|
||||||
percentComplete: eventData.percentComplete,
|
|
||||||
bytesLoaded: eventData.loaded,
|
|
||||||
bytesTotal: eventData.total,
|
|
||||||
bytesPerSecond: this.stats.speed,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._setProgressData(progressId, progressData);
|
|
||||||
}
|
|
||||||
|
|
||||||
_convertImageIdToDataSetUrl(imageId) {
|
|
||||||
// Remove the prefix ("dicomweb:" or "wadouri:"")
|
|
||||||
imageId = imageId.replace(/^(dicomweb:|wadouri:)/i, '');
|
|
||||||
|
|
||||||
// Remove "frame=999&" from the imageId
|
|
||||||
imageId = imageId.replace(/frame=\d+&?/i, '');
|
|
||||||
|
|
||||||
// Remove the last "&" like in "http://...?foo=1&bar=2&"
|
|
||||||
imageId = imageId.replace(/&$/, '');
|
|
||||||
|
|
||||||
return imageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDataSetUrl(stack) {
|
|
||||||
const imageId = stack.imageIds[0];
|
|
||||||
return this._convertImageIdToDataSetUrl(imageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StackLoadingListener extends BaseLoadingListener {
|
|
||||||
constructor(stack, options = {}) {
|
|
||||||
options.statsItemsLimit = 20;
|
|
||||||
super(stack, options);
|
|
||||||
|
|
||||||
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
|
|
||||||
this.framesStatus = this._createArray(stack.imageIds.length, false);
|
|
||||||
this.loadedCount = 0;
|
|
||||||
|
|
||||||
// Check how many instances has already been download (cached)
|
|
||||||
this._checkCachedData();
|
|
||||||
}
|
|
||||||
|
|
||||||
_convertImageIdsArrayToMap(imageIds) {
|
|
||||||
const imageIdsMap = new Map();
|
|
||||||
|
|
||||||
for (let i = 0; i < imageIds.length; i++) {
|
|
||||||
imageIdsMap.set(imageIds[i], {
|
|
||||||
index: i,
|
|
||||||
loaded: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageIdsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
_createArray(length, defaultValue) {
|
|
||||||
// `new Array(length)` is an anti-pattern in javascript because its
|
|
||||||
// funny API. Otherwise I would go for `new Array(length).fill(false)`
|
|
||||||
const array = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
array[i] = defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkCachedData() {
|
|
||||||
// const imageIds = this.stack.imageIds;
|
|
||||||
// TODO: No way to check status of Promise.
|
|
||||||
/*for(let i = 0; i < imageIds.length; i++) {
|
|
||||||
const imageId = imageIds[i];
|
|
||||||
|
|
||||||
const imagePromise = cornerstone.imageCache.getImageLoadObject(imageId).promise;
|
|
||||||
|
|
||||||
if (imagePromise && (imagePromise.state() === 'resolved')) {
|
|
||||||
this._updateFrameStatus(imageId, true);
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
_getImageLoadedEventName() {
|
|
||||||
return `${cornerstone.EVENTS.IMAGE_LOADED}.${this.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getImageCachePromiseRemoveEventName() {
|
|
||||||
return `${cornerstone.EVENTS.IMAGE_CACHE_PROMISE_REMOVED}.${this.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageLoadedEventHandler(e) {
|
|
||||||
this._updateFrameStatus(e.detail.image.imageId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageCachePromiseRemovedEventHandler(e) {
|
|
||||||
this._updateFrameStatus(e.detail.imageId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
startListening() {
|
|
||||||
const imageLoadedEventName = this._getImageLoadedEventName();
|
|
||||||
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
|
||||||
|
|
||||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
|
||||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
this.stopListening();
|
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
imageLoadedEventName,
|
|
||||||
this.imageLoadedEventHandler
|
|
||||||
);
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
imageCachePromiseRemovedEventName,
|
|
||||||
this.imageCachePromiseRemovedEventHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopListening() {
|
|
||||||
const imageLoadedEventName = this._getImageLoadedEventName();
|
|
||||||
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
|
||||||
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
imageLoadedEventName,
|
|
||||||
this.imageLoadedEventHandler
|
|
||||||
);
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
imageCachePromiseRemovedEventName,
|
|
||||||
this.imageCachePromiseRemovedEventHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateFrameStatus(imageId, loaded) {
|
|
||||||
const imageData = this.imageDataMap.get(imageId);
|
|
||||||
|
|
||||||
if (!imageData || imageData.loaded === loaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add one more frame to the stats
|
|
||||||
if (loaded) {
|
|
||||||
this._addStatsData(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
imageData.loaded = loaded;
|
|
||||||
this.framesStatus[imageData.index] = loaded;
|
|
||||||
this.loadedCount += loaded ? 1 : -1;
|
|
||||||
this._updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
_setProgressData(progressId, progressData) {
|
|
||||||
// TODO: This method (and _clearProgressById) need to access
|
|
||||||
// the Redux store and should therefore be provided from the
|
|
||||||
// application. I've added a workaround to pass this in through
|
|
||||||
// the 'options' variable on instantiation, but this is really ugly.
|
|
||||||
// We could consider making the StudyLoadingListener a higher-order
|
|
||||||
// component which would set this stuff itself.
|
|
||||||
throw new Error(
|
|
||||||
"The _setProgressData function must be provided in StudyLoadingListener's options"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearProgressById(progressId) {
|
|
||||||
throw new Error(
|
|
||||||
"The _clearProgressById function must be provided in StudyLoadingListener's options"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateProgress() {
|
|
||||||
const totalFramesCount = this.stack.imageIds.length;
|
|
||||||
const loadedFramesCount = this.loadedCount;
|
|
||||||
const loadingFramesCount = totalFramesCount - loadedFramesCount;
|
|
||||||
const percentComplete = Math.round(
|
|
||||||
(loadedFramesCount / totalFramesCount) * 100
|
|
||||||
);
|
|
||||||
const progressId = this._getProgressId();
|
|
||||||
const progressData = {
|
|
||||||
multiFrame: true,
|
|
||||||
totalFramesCount,
|
|
||||||
loadedFramesCount,
|
|
||||||
loadingFramesCount,
|
|
||||||
percentComplete,
|
|
||||||
framesPerSecond: this.stats.speed,
|
|
||||||
framesStatus: this.framesStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._setProgressData(progressId, progressData);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logProgress() {
|
|
||||||
const totalFramesCount = this.stack.imageIds.length;
|
|
||||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
|
||||||
let progressBar = '[';
|
|
||||||
|
|
||||||
for (let i = 0; i < totalFramesCount; i++) {
|
|
||||||
const ch = this.framesStatus[i] ? '|' : '.';
|
|
||||||
progressBar += `${ch}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
progressBar += ']';
|
|
||||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StudyLoadingListener {
|
|
||||||
constructor(options) {
|
|
||||||
this.listeners = {};
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
addStack(stack, stackMetaData) {
|
|
||||||
// TODO: Make this work for plugins
|
|
||||||
if (!stack) {
|
|
||||||
//console.log('Skipping adding stack to StudyLoadingListener');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displaySetInstanceUID = stack.displaySetInstanceUID;
|
|
||||||
|
|
||||||
if (!this.listeners[displaySetInstanceUID]) {
|
|
||||||
const listener = this._createListener(stack, stackMetaData);
|
|
||||||
if (listener) {
|
|
||||||
this.listeners[displaySetInstanceUID] = listener;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addStudy(study) {
|
|
||||||
study.displaySets.forEach(displaySet => {
|
|
||||||
const stack = StackManager.findOrCreateStack(study, displaySet);
|
|
||||||
|
|
||||||
// TODO: Make this work for plugins
|
|
||||||
if (!stack) {
|
|
||||||
console.warn('Skipping adding displaySet to StudyLoadingListener');
|
|
||||||
console.warn(displaySet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addStack(stack, {
|
|
||||||
isMultiFrame: displaySet.isMultiFrame,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addStudies(studies) {
|
|
||||||
if (!studies || !studies.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
studies.forEach(study => this.addStudy(study));
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
const displaySetInstanceUIDs = Object.keys(this.listeners);
|
|
||||||
const length = displaySetInstanceUIDs.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const displaySetInstanceUID = displaySetInstanceUIDs[i];
|
|
||||||
const displaySet = this.listeners[displaySetInstanceUID];
|
|
||||||
|
|
||||||
displaySet.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
_createListener(stack, stackMetaData) {
|
|
||||||
const schema = this._getSchema(stack);
|
|
||||||
|
|
||||||
// A StackLoadingListener can be created if it's wadors or not a multiframe
|
|
||||||
// wadouri instance (single file) that means "N" files will have to be
|
|
||||||
// downloaded where "N" is the number of frames. DICOMFileLoadingListener
|
|
||||||
// is created only if it's a single DICOM file and there's no way to know
|
|
||||||
// how many frames has already been loaded (bytes/s instead of frames/s).
|
|
||||||
if (schema === 'wadors' || !stackMetaData.isMultiFrame) {
|
|
||||||
return new StackLoadingListener(stack, this.options);
|
|
||||||
} else {
|
|
||||||
return new DICOMFileLoadingListener(stack, this.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getSchema(stack) {
|
|
||||||
const imageId = stack.imageIds[0];
|
|
||||||
const colonIndex = imageId.indexOf(':');
|
|
||||||
return imageId.substring(0, colonIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton
|
|
||||||
static getInstance(options) {
|
|
||||||
if (!StudyLoadingListener._instance) {
|
|
||||||
StudyLoadingListener._instance = new StudyLoadingListener(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StudyLoadingListener._instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { StudyLoadingListener, StackLoadingListener, DICOMFileLoadingListener };
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import OHIFError from './OHIFError';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class to fetch study metadata.
|
|
||||||
*/
|
|
||||||
export class StudyMetadataSource {
|
|
||||||
/**
|
|
||||||
* Get study metadata for a study with given study InstanceUID.
|
|
||||||
* @param {String} studyInstanceUID Study InstanceUID.
|
|
||||||
*/
|
|
||||||
getByInstanceUID(studyInstanceUID) {
|
|
||||||
/**
|
|
||||||
* Please override this method on a specialized class.
|
|
||||||
*/
|
|
||||||
throw new OHIFError(
|
|
||||||
'StudyMetadataSource::getByInstanceUID is not overriden. Please, override it in a specialized class. See OHIFStudyMetadataSource for example'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load study info and study metadata for a given study into the viewer.
|
|
||||||
* @param {StudyMetadata} study StudyMetadata object.
|
|
||||||
*/
|
|
||||||
loadStudy(study) {
|
|
||||||
/**
|
|
||||||
* Please override this method on a specialized class.
|
|
||||||
*/
|
|
||||||
throw new OHIFError(
|
|
||||||
'StudyMetadataSource::loadStudy is not overriden. Please, override it in a specialized class. See OHIFStudyMetadataSource for example'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,261 +0,0 @@
|
|||||||
import log from '../log.js';
|
|
||||||
import OHIFError from './OHIFError';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
import getImageId from '../utils/getImageId.js';
|
|
||||||
|
|
||||||
export class StudyPrefetcher {
|
|
||||||
constructor(studies) {
|
|
||||||
this.studies = studies || [];
|
|
||||||
this.prefetchDisplaySetsTimeout = 300;
|
|
||||||
this.lastActiveViewportElement = null;
|
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
'cornerstoneimagecachefull.StudyPrefetcher',
|
|
||||||
this.cacheFullHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stopPrefetching();
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
'cornerstoneimagecachefull.StudyPrefetcher',
|
|
||||||
this.cacheFullHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInstance() {
|
|
||||||
if (!StudyPrefetcher.instance) {
|
|
||||||
StudyPrefetcher.instance = new StudyPrefetcher([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StudyPrefetcher.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStudies(studies) {
|
|
||||||
this.stopPrefetching();
|
|
||||||
this.studies = studies;
|
|
||||||
}
|
|
||||||
|
|
||||||
prefetch() {
|
|
||||||
if (!this.studies || !this.studies.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stopPrefetching();
|
|
||||||
this.prefetchDisplaySets();
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPrefetching() {
|
|
||||||
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
|
|
||||||
}
|
|
||||||
|
|
||||||
prefetchDisplaySetsAsync(timeout) {
|
|
||||||
timeout = timeout || this.prefetchDisplaySetsTimeout;
|
|
||||||
|
|
||||||
clearTimeout(this.prefetchDisplaySetsHandler);
|
|
||||||
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
|
||||||
this.prefetchDisplaySets();
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefetchDisplaySets() {
|
|
||||||
// TODO: Allow passing in config
|
|
||||||
let config = {
|
|
||||||
order: 'closest',
|
|
||||||
displaySetCount: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
|
|
||||||
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
|
|
||||||
|
|
||||||
this.prefetchImageIds(imageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefetchImageIds(imageIds) {
|
|
||||||
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
|
||||||
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
|
||||||
const requestType = 'prefetch';
|
|
||||||
const preventCache = false;
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
nonCachedImageIds.forEach(imageId => {
|
|
||||||
requestPoolManager.addRequest(
|
|
||||||
{},
|
|
||||||
imageId,
|
|
||||||
requestType,
|
|
||||||
preventCache,
|
|
||||||
noop,
|
|
||||||
noop
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
requestPoolManager.startGrabbing();
|
|
||||||
}
|
|
||||||
|
|
||||||
getStudy(image) {
|
|
||||||
const StudyInstanceUID = cornerstone.metaData.get(
|
|
||||||
'StudyInstanceUID',
|
|
||||||
image.imageId
|
|
||||||
);
|
|
||||||
return OHIF.viewer.Studies.find(
|
|
||||||
study => study.StudyInstanceUID === StudyInstanceUID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSeries(study, image) {
|
|
||||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
|
||||||
'SeriesInstanceUID',
|
|
||||||
image.imageId
|
|
||||||
);
|
|
||||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
|
||||||
|
|
||||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInstance(series, image) {
|
|
||||||
const instanceMetadata = cornerstone.metaData.get(
|
|
||||||
'instance',
|
|
||||||
image.imageId
|
|
||||||
);
|
|
||||||
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveDisplaySet(displaySets, instance) {
|
|
||||||
return displaySets.find(displaySet => {
|
|
||||||
return displaySet.images.some(displaySetImage => {
|
|
||||||
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisplaySetsToPrefetch(config) {
|
|
||||||
const image = this.getActiveViewportImage();
|
|
||||||
|
|
||||||
if (!image || !config || !config.displaySetCount) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/*const study = this.getStudy(image);
|
|
||||||
const series = this.getSeries(study, image);
|
|
||||||
const instance = this.getInstance(series, image);*/
|
|
||||||
const displaySets = study.displaySets;
|
|
||||||
const activeDisplaySet = null; //this.getActiveDisplaySet(displaySets, instance);
|
|
||||||
const prefetchMethodMap = {
|
|
||||||
topdown: 'getFirstDisplaySets',
|
|
||||||
downward: 'getNextDisplaySets',
|
|
||||||
closest: 'getClosestDisplaySets',
|
|
||||||
};
|
|
||||||
|
|
||||||
const prefetchOrder = config.order;
|
|
||||||
const methodName = prefetchMethodMap[prefetchOrder];
|
|
||||||
const getDisplaySets = this[methodName];
|
|
||||||
|
|
||||||
if (!getDisplaySets) {
|
|
||||||
if (prefetchOrder) {
|
|
||||||
log.warn(`Invalid prefetch order configuration (${prefetchOrder})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDisplaySets.call(
|
|
||||||
this,
|
|
||||||
displaySets,
|
|
||||||
activeDisplaySet,
|
|
||||||
config.displaySetCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
|
||||||
const length = displaySets.length;
|
|
||||||
const selectedDisplaySets = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < length && displaySetCount; i++) {
|
|
||||||
const displaySet = displaySets[i];
|
|
||||||
|
|
||||||
if (displaySet !== activeDisplaySet) {
|
|
||||||
selectedDisplaySets.push(displaySet);
|
|
||||||
displaySetCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedDisplaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
|
||||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
|
||||||
const begin = activeDisplaySetIndex + 1;
|
|
||||||
const end = Math.min(begin + displaySetCount, displaySets.length);
|
|
||||||
|
|
||||||
return displaySets.slice(begin, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
|
||||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
|
||||||
const length = displaySets.length;
|
|
||||||
const selectedDisplaySets = [];
|
|
||||||
let left = activeDisplaySetIndex - 1;
|
|
||||||
let right = activeDisplaySetIndex + 1;
|
|
||||||
|
|
||||||
while ((left >= 0 || right < length) && displaySetCount) {
|
|
||||||
if (left >= 0) {
|
|
||||||
selectedDisplaySets.push(displaySets[left]);
|
|
||||||
displaySetCount--;
|
|
||||||
left--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right < length && displaySetCount) {
|
|
||||||
selectedDisplaySets.push(displaySets[right]);
|
|
||||||
displaySetCount--;
|
|
||||||
right++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedDisplaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageIdsFromDisplaySets(displaySets) {
|
|
||||||
let imageIds = [];
|
|
||||||
|
|
||||||
displaySets.forEach(displaySet => {
|
|
||||||
imageIds = imageIds.concat(this.getImageIdsFromDisplaySet(displaySet));
|
|
||||||
});
|
|
||||||
|
|
||||||
return imageIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageIdsFromDisplaySet(displaySet) {
|
|
||||||
const imageIds = [];
|
|
||||||
|
|
||||||
// TODO: This duplicates work done by the stack manager
|
|
||||||
displaySet.images.forEach(image => {
|
|
||||||
const numFrames = image.numFrames;
|
|
||||||
if (numFrames > 1) {
|
|
||||||
for (let i = 0; i < numFrames; i++) {
|
|
||||||
let imageId = getImageId(image, i);
|
|
||||||
imageIds.push(imageId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let imageId = getImageId(image);
|
|
||||||
imageIds.push(imageId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return imageIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterCachedImageIds(imageIds) {
|
|
||||||
return imageIds.filter(imageId => !this.isImageCached(imageId));
|
|
||||||
}
|
|
||||||
|
|
||||||
isImageCached(imageId) {
|
|
||||||
const image = cornerstone.imageCache.imageCache[imageId];
|
|
||||||
return image && image.sizeInBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheFullHandler = () => {
|
|
||||||
log.warn('Cache full');
|
|
||||||
this.stopPrefetching();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,508 +0,0 @@
|
|||||||
import guid from '../utils/guid';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants
|
|
||||||
*/
|
|
||||||
const PROPERTY_SEPARATOR = '.';
|
|
||||||
const ORDER_ASC = 'asc';
|
|
||||||
const ORDER_DESC = 'desc';
|
|
||||||
const MIN_COUNT = 0x00000000;
|
|
||||||
const MAX_COUNT = 0x7fffffff;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Definition
|
|
||||||
*/
|
|
||||||
export class TypeSafeCollection {
|
|
||||||
constructor() {
|
|
||||||
this._operationCount = MIN_COUNT;
|
|
||||||
this._elementList = [];
|
|
||||||
this._handlers = Object.create(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
_invalidate() {
|
|
||||||
let count = this._operationCount;
|
|
||||||
this._operationCount = count < MAX_COUNT ? count + 1 : MIN_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
_elements(silent) {
|
|
||||||
silent === true || this._operationCount;
|
|
||||||
return this._elementList;
|
|
||||||
}
|
|
||||||
|
|
||||||
_elementWithPayload(payload, silent) {
|
|
||||||
return this._elements(silent).find(item => item.payload === payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
_elementWithId(id, silent) {
|
|
||||||
return this._elements(silent).find(item => item.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_trigger(event, data) {
|
|
||||||
let handlers = this._handlers;
|
|
||||||
if (event in handlers) {
|
|
||||||
handlers = handlers[event];
|
|
||||||
if (!(handlers instanceof Array)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0, limit = handlers.length; i < limit; ++i) {
|
|
||||||
let handler = handlers[i];
|
|
||||||
if (_isFunction(handler)) {
|
|
||||||
handler.call(null, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
onInsert(callback) {
|
|
||||||
if (_isFunction(callback)) {
|
|
||||||
let handlers = this._handlers.insert;
|
|
||||||
if (!(handlers instanceof Array)) {
|
|
||||||
handlers = [];
|
|
||||||
this._handlers.insert = handlers;
|
|
||||||
}
|
|
||||||
handlers.push(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the payload associated with the given ID to be the new supplied payload.
|
|
||||||
* @param {string} id The ID of the entry that will be updated.
|
|
||||||
* @param {any} payload The element that will replace the previous payload.
|
|
||||||
* @returns {boolean} Returns true if the given ID is present in the collection, false otherwise.
|
|
||||||
*/
|
|
||||||
updateById(id, payload) {
|
|
||||||
let result = false,
|
|
||||||
found = this._elementWithPayload(payload, true);
|
|
||||||
if (found) {
|
|
||||||
// nothing to do since the element is already in the collection...
|
|
||||||
if (found.id === id) {
|
|
||||||
// set result to true since the ids match...
|
|
||||||
result = true;
|
|
||||||
this._invalidate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
found = this._elementWithId(id, true);
|
|
||||||
if (found) {
|
|
||||||
found.payload = payload;
|
|
||||||
result = true;
|
|
||||||
this._invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signal that the given element has been changed by notifying reactive data-source observers.
|
|
||||||
* This method is basically a means to invalidate the inernal reactive data-source.
|
|
||||||
* @param {any} payload The element that has been altered.
|
|
||||||
* @returns {boolean} Returns true if the element is present in the collection, false otherwise.
|
|
||||||
*/
|
|
||||||
update(payload) {
|
|
||||||
let result = false,
|
|
||||||
found = this._elementWithPayload(payload, true);
|
|
||||||
if (found) {
|
|
||||||
// nothing to do since the element is already in the collection...
|
|
||||||
result = true;
|
|
||||||
this._invalidate();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert an element in the collection. On success, the element ID (a unique string) is returned. On failure, returns null.
|
|
||||||
* A failure scenario only happens when the given payload is already present in the collection. Note that NO exceptions are thrown!
|
|
||||||
* @param {any} payload The element to be stored.
|
|
||||||
* @returns {string} The ID of the inserted element or null if the element already exists...
|
|
||||||
*/
|
|
||||||
insert(payload) {
|
|
||||||
let id = null,
|
|
||||||
found = this._elementWithPayload(payload, true);
|
|
||||||
if (!found) {
|
|
||||||
id = guid();
|
|
||||||
this._elements(true).push({ id, payload });
|
|
||||||
this._invalidate();
|
|
||||||
this._trigger('insert', { id, data: payload });
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all elements from the collection.
|
|
||||||
* @returns {void} No meaningful value is returned.
|
|
||||||
*/
|
|
||||||
removeAll() {
|
|
||||||
let all = this._elements(true),
|
|
||||||
length = all.length;
|
|
||||||
for (let i = length - 1; i >= 0; i--) {
|
|
||||||
let item = all[i];
|
|
||||||
delete item.id;
|
|
||||||
delete item.payload;
|
|
||||||
all[i] = null;
|
|
||||||
}
|
|
||||||
all.splice(0, length);
|
|
||||||
this._invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove elements from the collection that match the criteria given in the property map.
|
|
||||||
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
|
|
||||||
* @returns {Array} A list with all removed elements.
|
|
||||||
*/
|
|
||||||
remove(propertyMap) {
|
|
||||||
let found = this.findAllEntriesBy(propertyMap),
|
|
||||||
foundCount = found.length,
|
|
||||||
removed = [];
|
|
||||||
if (foundCount > 0) {
|
|
||||||
const all = this._elements(true);
|
|
||||||
for (let i = foundCount - 1; i >= 0; i--) {
|
|
||||||
let item = found[i];
|
|
||||||
all.splice(item[2], 1);
|
|
||||||
removed.push(item[0]);
|
|
||||||
}
|
|
||||||
this._invalidate();
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the ID of the given element inside the collection.
|
|
||||||
* @param {any} payload The element being searched for.
|
|
||||||
* @returns {string} The ID of the given element or undefined if the element is not present.
|
|
||||||
*/
|
|
||||||
getElementId(payload) {
|
|
||||||
let found = this._elementWithPayload(payload);
|
|
||||||
return found && found.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the position of the given element in the internal list returning -1 if the element is not present.
|
|
||||||
* @param {any} payload The element being searched for.
|
|
||||||
* @returns {number} The position of the given element in the internal list. If the element is not present -1 is returned.
|
|
||||||
*/
|
|
||||||
findById(id) {
|
|
||||||
let found = this._elementWithId(id);
|
|
||||||
return found && found.payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the position of the given element in the internal list returning -1 if the element is not present.
|
|
||||||
* @param {any} payload The element being searched for.
|
|
||||||
* @returns {number} The position of the given element in the internal list. If the element is not present -1 is returned.
|
|
||||||
*/
|
|
||||||
indexOfElement(payload) {
|
|
||||||
return this._elements().indexOf(this._elementWithPayload(payload, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the position of the element associated with the given ID in the internal list returning -1 if the element is not present.
|
|
||||||
* @param {string} id The index of the element.
|
|
||||||
* @returns {number} The position of the element associated with the given ID in the internal list. If the element is not present -1 is returned.
|
|
||||||
*/
|
|
||||||
indexOfId(id) {
|
|
||||||
return this._elements().indexOf(this._elementWithId(id, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a list-like approach to the collection returning an element by index.
|
|
||||||
* @param {number} index The index of the element.
|
|
||||||
* @returns {any} If out of bounds, undefined is returned. Otherwise the element in the given position is returned.
|
|
||||||
*/
|
|
||||||
getElementByIndex(index) {
|
|
||||||
let found = this._elements()[index >= 0 ? index : -1];
|
|
||||||
return found && found.payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find an element by a criteria defined by the given callback function.
|
|
||||||
* Attention!!! The reactive source will not be notified if no valid callback is supplied...
|
|
||||||
* @param {function} callback A callback function which will define the search criteria. The callback
|
|
||||||
* function will be passed the collection element, its ID and its index in this very order. The callback
|
|
||||||
* shall return true when its criterea has been fulfilled.
|
|
||||||
* @returns {any} The matched element or undefined if not match was found.
|
|
||||||
*/
|
|
||||||
find(callback) {
|
|
||||||
let found;
|
|
||||||
if (_isFunction(callback)) {
|
|
||||||
found = this._elements().find((item, index) => {
|
|
||||||
return callback.call(this, item.payload, item.id, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return found && found.payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the first element that strictly matches the specified property map.
|
|
||||||
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
|
|
||||||
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
|
|
||||||
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
|
|
||||||
* but is not valid, an exception will be thrown.
|
|
||||||
* @returns {Any} The matched element or undefined if not match was found.
|
|
||||||
*/
|
|
||||||
findBy(propertyMap, options) {
|
|
||||||
let found;
|
|
||||||
if (_isObject(options)) {
|
|
||||||
// if the "options" argument is provided and is a valid object,
|
|
||||||
// it must be applied to the dataset before search...
|
|
||||||
const all = this.all(options);
|
|
||||||
if (all.length > 0) {
|
|
||||||
if (_isObject(propertyMap)) {
|
|
||||||
found = all.find(item =>
|
|
||||||
_compareToPropertyMapStrict(propertyMap, item)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
found = all[0]; // simply extract the first element...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (_isObject(propertyMap)) {
|
|
||||||
found = this._elements().find(item =>
|
|
||||||
_compareToPropertyMapStrict(propertyMap, item.payload)
|
|
||||||
);
|
|
||||||
if (found) {
|
|
||||||
found = found.payload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all elements that strictly match the specified property map.
|
|
||||||
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
|
|
||||||
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
|
|
||||||
* @returns {Array} An array of entries of all elements that match the given criteria. Each set in
|
|
||||||
* in the array has the following format: [ elementData, elementId, elementIndex ].
|
|
||||||
*/
|
|
||||||
findAllEntriesBy(propertyMap) {
|
|
||||||
const found = [];
|
|
||||||
if (_isObject(propertyMap)) {
|
|
||||||
this._elements().forEach((item, index) => {
|
|
||||||
if (_compareToPropertyMapStrict(propertyMap, item.payload)) {
|
|
||||||
// Match! Add it to the found list...
|
|
||||||
found.push([item.payload, item.id, index]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all elements that match a specified property map.
|
|
||||||
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
|
|
||||||
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
|
|
||||||
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
|
|
||||||
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
|
|
||||||
* but is not valid, an exception will be thrown.
|
|
||||||
* @returns {Array} An array with all elements that match the given criteria and sorted in the specified sorting order.
|
|
||||||
*/
|
|
||||||
findAllBy(propertyMap, options) {
|
|
||||||
const found = this.findAllEntriesBy(propertyMap).map(item => item[0]); // Only payload is relevant...
|
|
||||||
if (_isObject(options)) {
|
|
||||||
if ('sort' in options) {
|
|
||||||
_sortListBy(found, options.sort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the supplied callback function for each element of the collection.
|
|
||||||
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
|
|
||||||
* @param {function} callback The callback function to be executed. The callback is passed the element,
|
|
||||||
* its ID and its index in this very order.
|
|
||||||
* @returns {void} Nothing is returned.
|
|
||||||
*/
|
|
||||||
forEach(callback) {
|
|
||||||
if (_isFunction(callback)) {
|
|
||||||
this._elements().forEach((item, index) => {
|
|
||||||
callback.call(this, item.payload, item.id, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count the number of elements currently in the collection.
|
|
||||||
* @returns {number} The current number of elements in the collection.
|
|
||||||
*/
|
|
||||||
count() {
|
|
||||||
return this._elements().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list with all elements of the collection optionally sorted by a sorting specifier criteria.
|
|
||||||
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
|
|
||||||
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
|
|
||||||
* but is not valid, an exception will be thrown.
|
|
||||||
* @returns {Array} An array with all elements stored in the collection.
|
|
||||||
*/
|
|
||||||
all(options) {
|
|
||||||
let list = this._elements().map(item => item.payload);
|
|
||||||
if (_isObject(options)) {
|
|
||||||
if ('sort' in options) {
|
|
||||||
_sortListBy(list, options.sort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if supplied argument is a valid object for current class purposes.
|
|
||||||
* Atention! The underscore version of this function should not be used for performance reasons.
|
|
||||||
*/
|
|
||||||
function _isObject(subject) {
|
|
||||||
return (
|
|
||||||
subject instanceof Object ||
|
|
||||||
(typeof subject === 'object' && subject !== null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if supplied argument is a valid string for current class purposes.
|
|
||||||
* Atention! The underscore version of this function should not be used for performance reasons.
|
|
||||||
*/
|
|
||||||
function _isString(subject) {
|
|
||||||
return typeof subject === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if supplied argument is a valid function for current class purposes.
|
|
||||||
* Atention! The underscore version of this function should not be used for performance reasons.
|
|
||||||
*/
|
|
||||||
function _isFunction(subject) {
|
|
||||||
return typeof subject === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut for Object's prototype "hasOwnProperty" method.
|
|
||||||
*/
|
|
||||||
const _hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an object's property value by name. Composite property names (e.g., 'address.country.name') are accepted.
|
|
||||||
* @param {Object} targetObject The object we want read the property from...
|
|
||||||
* @param {String} propertyName The property to be read (e.g., 'address.street.name' or 'address.street.number'
|
|
||||||
* to read object.address.street.name or object.address.street.number, respectively);
|
|
||||||
* @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached.
|
|
||||||
*/
|
|
||||||
function _getPropertyValue(targetObject, propertyName) {
|
|
||||||
let propertyValue; // undefined (the default return value)
|
|
||||||
if (_isObject(targetObject) && _isString(propertyName)) {
|
|
||||||
const fragments = propertyName.split(PROPERTY_SEPARATOR);
|
|
||||||
const fragmentCount = fragments.length;
|
|
||||||
if (fragmentCount > 0) {
|
|
||||||
const firstFragment = fragments[0];
|
|
||||||
const remainingFragments =
|
|
||||||
fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null;
|
|
||||||
propertyValue = targetObject[firstFragment];
|
|
||||||
if (remainingFragments !== null) {
|
|
||||||
propertyValue = _getPropertyValue(propertyValue, remainingFragments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return propertyValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare a property map with a target object using strict comparison.
|
|
||||||
* @param {Object} propertyMap The property map whose properties will be used for comparison. Composite
|
|
||||||
* property names (e.g., 'address.country.name') will be tested against the "resolved" properties from the target object.
|
|
||||||
* @param {Object} targetObject The target object whose properties will be tested.
|
|
||||||
* @returns {boolean} Returns true if the properties match, false otherwise.
|
|
||||||
*/
|
|
||||||
function _compareToPropertyMapStrict(propertyMap, targetObject) {
|
|
||||||
let result = false;
|
|
||||||
// "for in" loops do not thown exceptions for invalid data types...
|
|
||||||
for (let propertyName in propertyMap) {
|
|
||||||
if (_hasOwnProperty.call(propertyMap, propertyName)) {
|
|
||||||
if (
|
|
||||||
propertyMap[propertyName] !==
|
|
||||||
_getPropertyValue(targetObject, propertyName)
|
|
||||||
) {
|
|
||||||
result = false;
|
|
||||||
break;
|
|
||||||
} else if (result !== true) {
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a sorting specifier is valid.
|
|
||||||
* A valid sorting specifier consists of an array of arrays being each subarray a pair
|
|
||||||
* in the format ["property name", "sorting order"].
|
|
||||||
* The following exemple can be used to sort studies by "date"" and use "time" to break ties in descending order.
|
|
||||||
* [ [ 'study.date', 'desc' ], [ 'study.time', 'desc' ] ]
|
|
||||||
* @param {Array} specifiers The sorting specifier to be tested.
|
|
||||||
* @returns {boolean} Returns true if the specifiers are valid, false otherwise.
|
|
||||||
*/
|
|
||||||
function _isValidSortingSpecifier(specifiers) {
|
|
||||||
let result = true;
|
|
||||||
if (specifiers instanceof Array && specifiers.length > 0) {
|
|
||||||
for (let i = specifiers.length - 1; i >= 0; i--) {
|
|
||||||
const item = specifiers[i];
|
|
||||||
if (item instanceof Array) {
|
|
||||||
const property = item[0];
|
|
||||||
const order = item[1];
|
|
||||||
if (
|
|
||||||
_isString(property) &&
|
|
||||||
(order === ORDER_ASC || order === ORDER_DESC)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts an array based on sorting specifier options.
|
|
||||||
* @param {Array} list The that needs to be sorted.
|
|
||||||
* @param {Array} specifiers An array of specifiers. Please read isValidSortingSpecifier method definition for further details.
|
|
||||||
* @returns {void} No value is returned. The array is sorted in place.
|
|
||||||
*/
|
|
||||||
function _sortListBy(list, specifiers) {
|
|
||||||
if (list instanceof Array && _isValidSortingSpecifier(specifiers)) {
|
|
||||||
const specifierCount = specifiers.length;
|
|
||||||
list.sort(function _sortListByCallback(a, b) {
|
|
||||||
// callback name for stack traces...
|
|
||||||
let index = 0;
|
|
||||||
while (index < specifierCount) {
|
|
||||||
const specifier = specifiers[index];
|
|
||||||
const property = specifier[0];
|
|
||||||
const order = specifier[1] === ORDER_DESC ? -1 : 1;
|
|
||||||
const aValue = _getPropertyValue(a, property);
|
|
||||||
const bValue = _getPropertyValue(b, property);
|
|
||||||
// @TODO: should we check for the types being compared, like:
|
|
||||||
// ~~ if (typeof aValue !== typeof bValue) continue;
|
|
||||||
// Not sure because dates, for example, can be correctly compared to numbers...
|
|
||||||
if (aValue < bValue) {
|
|
||||||
return order * -1;
|
|
||||||
}
|
|
||||||
if (aValue > bValue) {
|
|
||||||
return order * 1;
|
|
||||||
}
|
|
||||||
if (++index >= specifierCount) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid Arguments');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +1,23 @@
|
|||||||
import { InstanceMetadata, SeriesMetadata, StudyMetadata } from './metadata';
|
|
||||||
|
|
||||||
import CommandsManager from './CommandsManager.js';
|
import CommandsManager from './CommandsManager.js';
|
||||||
import { DICOMFileLoadingListener } from './StudyLoadingListener';
|
|
||||||
import HotkeysManager from './HotkeysManager.js';
|
import HotkeysManager from './HotkeysManager.js';
|
||||||
import ImageSet from './ImageSet';
|
import ImageSet from './ImageSet';
|
||||||
import MetadataProvider from './MetadataProvider';
|
import MetadataProvider from './MetadataProvider';
|
||||||
import OHIFError from './OHIFError.js';
|
import OHIFError from './OHIFError.js';
|
||||||
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
|
|
||||||
import { StackLoadingListener } from './StudyLoadingListener';
|
|
||||||
import { StudyLoadingListener } from './StudyLoadingListener';
|
|
||||||
import { StudyMetadataSource } from './StudyMetadataSource';
|
|
||||||
import { StudyPrefetcher } from './StudyPrefetcher';
|
|
||||||
import { TypeSafeCollection } from './TypeSafeCollection';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OHIFStudyMetadataSource,
|
|
||||||
MetadataProvider,
|
MetadataProvider,
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
HotkeysManager,
|
HotkeysManager,
|
||||||
ImageSet,
|
ImageSet,
|
||||||
StudyPrefetcher,
|
|
||||||
StudyLoadingListener,
|
|
||||||
StackLoadingListener,
|
|
||||||
DICOMFileLoadingListener,
|
|
||||||
StudyMetadata,
|
|
||||||
SeriesMetadata,
|
|
||||||
InstanceMetadata,
|
|
||||||
TypeSafeCollection,
|
|
||||||
OHIFError,
|
OHIFError,
|
||||||
StudyMetadataSource,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
OHIFStudyMetadataSource,
|
|
||||||
MetadataProvider,
|
MetadataProvider,
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
HotkeysManager,
|
HotkeysManager,
|
||||||
ImageSet,
|
ImageSet,
|
||||||
StudyPrefetcher,
|
|
||||||
StudyLoadingListener,
|
|
||||||
StackLoadingListener,
|
|
||||||
DICOMFileLoadingListener,
|
|
||||||
StudyMetadata,
|
|
||||||
SeriesMetadata,
|
|
||||||
InstanceMetadata,
|
|
||||||
TypeSafeCollection,
|
|
||||||
OHIFError,
|
OHIFError,
|
||||||
StudyMetadataSource,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default classes;
|
export default classes;
|
||||||
|
|||||||
@ -1,210 +0,0 @@
|
|||||||
import { Metadata } from './Metadata';
|
|
||||||
import OHIFError from '../OHIFError.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ATTENTION! This class should never depend on StudyMetadata or SeriesMetadata classes as this could
|
|
||||||
* possibly cause circular dependency issues.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const UNDEFINED = 'undefined';
|
|
||||||
const STRING = 'string';
|
|
||||||
|
|
||||||
export class InstanceMetadata extends Metadata {
|
|
||||||
constructor(data, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
// Initialize Private Properties
|
|
||||||
Object.defineProperties(this, {
|
|
||||||
_imageId: {
|
|
||||||
configurable: true, // configurable so that it can be redefined in sub-classes...
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Initialize Public Properties
|
|
||||||
this._definePublicProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define Public Properties
|
|
||||||
* This method should only be called during initialization (inside the class constructor)
|
|
||||||
*/
|
|
||||||
_definePublicProperties() {
|
|
||||||
/**
|
|
||||||
* Property: this.SOPInstanceUID
|
|
||||||
* Same as this.getSOPInstanceUID()
|
|
||||||
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
|
|
||||||
* sopInstanceCollection.findBy({
|
|
||||||
* SOPInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
Object.defineProperty(this, 'SOPInstanceUID', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
get: function() {
|
|
||||||
return this.getSOPInstanceUID();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
|
|
||||||
*/
|
|
||||||
getStudyInstanceUID() {
|
|
||||||
return this.getTagValue('StudyInstanceUID', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
|
|
||||||
*/
|
|
||||||
getSeriesInstanceUID() {
|
|
||||||
return this.getTagValue('SeriesInstanceUID', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the SOPInstanceUID of the current instance.
|
|
||||||
*/
|
|
||||||
getSOPInstanceUID() {
|
|
||||||
return this.getTagValue('SOPInstanceUID', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Improve this... (E.g.: blob data)
|
|
||||||
getStringValue(tagOrProperty, index, defaultValue) {
|
|
||||||
let value = this.getTagValue(tagOrProperty, defaultValue);
|
|
||||||
|
|
||||||
if (typeof value !== STRING && typeof value !== UNDEFINED) {
|
|
||||||
value = value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Improve this... (E.g.: blob data)
|
|
||||||
getFloatValue(tagOrProperty, index, defaultValue) {
|
|
||||||
let value = this.getTagValue(tagOrProperty, defaultValue);
|
|
||||||
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
|
||||||
|
|
||||||
if (value instanceof Array) {
|
|
||||||
value.forEach((val, idx) => {
|
|
||||||
value[idx] = parseFloat(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof value === STRING ? parseFloat(value) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Improve this... (E.g.: blob data)
|
|
||||||
getIntValue(tagOrProperty, index, defaultValue) {
|
|
||||||
let value = this.getTagValue(tagOrProperty, defaultValue);
|
|
||||||
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
|
||||||
|
|
||||||
if (value instanceof Array) {
|
|
||||||
value.forEach((val, idx) => {
|
|
||||||
value[idx] = parseFloat(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof value === STRING ? parseInt(value) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
|
|
||||||
*/
|
|
||||||
getTagValue(tagOrProperty, defaultValue) {
|
|
||||||
/**
|
|
||||||
* Please override this method on a specialized class.
|
|
||||||
*/
|
|
||||||
throw new OHIFError(
|
|
||||||
'InstanceMetadata::getTagValue is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares the current instance with another one.
|
|
||||||
* @param {InstanceMetadata} instance An instance of the InstanceMetadata class.
|
|
||||||
* @returns {boolean} Returns true if both instances refer to the same instance.
|
|
||||||
*/
|
|
||||||
equals(instance) {
|
|
||||||
const self = this;
|
|
||||||
return (
|
|
||||||
instance === self ||
|
|
||||||
(instance instanceof InstanceMetadata &&
|
|
||||||
instance.getSOPInstanceUID() === self.getSOPInstanceUID())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the tagOrProperty exists
|
|
||||||
* @param {String} tagOrProperty tag or property be checked
|
|
||||||
* @return {Boolean} True if the tag or property exists or false if doesn't
|
|
||||||
*/
|
|
||||||
tagExists(tagOrProperty) {
|
|
||||||
/**
|
|
||||||
* Please override this method
|
|
||||||
*/
|
|
||||||
throw new OHIFError(
|
|
||||||
'InstanceMetadata::tagExists is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get custom image id of a sop instance
|
|
||||||
* @return {Any} sop instance image id
|
|
||||||
*/
|
|
||||||
getImageId(frame) {
|
|
||||||
/**
|
|
||||||
* Please override this method
|
|
||||||
*/
|
|
||||||
throw new OHIFError(
|
|
||||||
'InstanceMetadata::getImageId is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an value based that can be index based. This function is called by all getters. See above functions.
|
|
||||||
* - If value is a String and has indexes:
|
|
||||||
* - If undefined index: returns an array of the split values.
|
|
||||||
* - If defined index:
|
|
||||||
* - If invalid: returns defaultValue
|
|
||||||
* - If valid: returns the indexed value
|
|
||||||
* - If value is not a String, returns default value.
|
|
||||||
*/
|
|
||||||
static getIndexedValue(value, index, defaultValue) {
|
|
||||||
let result = defaultValue;
|
|
||||||
|
|
||||||
if (typeof value === STRING) {
|
|
||||||
const hasIndexValues = value.indexOf('\\') !== -1;
|
|
||||||
|
|
||||||
result = value;
|
|
||||||
|
|
||||||
if (hasIndexValues) {
|
|
||||||
const splitValues = value.split('\\');
|
|
||||||
if (Metadata.isValidIndex(index)) {
|
|
||||||
const indexedValue = splitValues[index];
|
|
||||||
|
|
||||||
result = typeof indexedValue !== STRING ? defaultValue : indexedValue;
|
|
||||||
} else {
|
|
||||||
result = splitValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* Constants
|
|
||||||
*/
|
|
||||||
|
|
||||||
const STRING = 'string';
|
|
||||||
const NUMBER = 'number';
|
|
||||||
const FUNCTION = 'function';
|
|
||||||
const OBJECT = 'object';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Definition
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class Metadata {
|
|
||||||
/**
|
|
||||||
* Constructor and Instance Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor(data, uid) {
|
|
||||||
// Define the main "_data" private property as an immutable property.
|
|
||||||
// IMPORTANT: This property can only be set during instance construction.
|
|
||||||
Object.defineProperty(this, '_data', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the main "_uid" private property as an immutable property.
|
|
||||||
// IMPORTANT: This property can only be set during instance construction.
|
|
||||||
Object.defineProperty(this, '_uid', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: uid,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define "_custom" properties as an immutable property.
|
|
||||||
// IMPORTANT: This property can only be set during instance construction.
|
|
||||||
Object.defineProperty(this, '_custom', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: Object.create(null),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getData() {
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataProperty(propertyName) {
|
|
||||||
let propertyValue;
|
|
||||||
const _data = this._data;
|
|
||||||
if (
|
|
||||||
_data instanceof Object ||
|
|
||||||
(typeof _data === OBJECT && _data !== null)
|
|
||||||
) {
|
|
||||||
propertyValue = _data[propertyName];
|
|
||||||
}
|
|
||||||
return propertyValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get unique object ID
|
|
||||||
*/
|
|
||||||
getObjectID() {
|
|
||||||
return this._uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set custom attribute value
|
|
||||||
* @param {String} attribute Custom attribute name
|
|
||||||
* @param {Any} value Custom attribute value
|
|
||||||
*/
|
|
||||||
setCustomAttribute(attribute, value) {
|
|
||||||
this._custom[attribute] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get custom attribute value
|
|
||||||
* @param {String} attribute Custom attribute name
|
|
||||||
* @return {Any} Custom attribute value
|
|
||||||
*/
|
|
||||||
getCustomAttribute(attribute) {
|
|
||||||
return this._custom[attribute];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a custom attribute exists
|
|
||||||
* @param {String} attribute Custom attribute name
|
|
||||||
* @return {Boolean} True if custom attribute exists or false if not
|
|
||||||
*/
|
|
||||||
customAttributeExists(attribute) {
|
|
||||||
return attribute in this._custom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set custom attributes in batch mode.
|
|
||||||
* @param {Object} attributeMap An object whose own properties will be used as custom attributes.
|
|
||||||
*/
|
|
||||||
setCustomAttributes(attributeMap) {
|
|
||||||
const _hasOwn = Object.prototype.hasOwnProperty;
|
|
||||||
const _custom = this._custom;
|
|
||||||
for (let attribute in attributeMap) {
|
|
||||||
if (_hasOwn.call(attributeMap, attribute)) {
|
|
||||||
_custom[attribute] = attributeMap[attribute];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
static isValidUID(uid) {
|
|
||||||
return typeof uid === STRING && uid.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static isValidIndex(index) {
|
|
||||||
return typeof index === NUMBER && index >= 0 && (index | 0) === index;
|
|
||||||
}
|
|
||||||
|
|
||||||
static isValidCallback(callback) {
|
|
||||||
return typeof callback === FUNCTION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import { InstanceMetadata } from './InstanceMetadata';
|
|
||||||
import getImageId from '../../utils/getImageId.js';
|
|
||||||
|
|
||||||
export class OHIFInstanceMetadata extends InstanceMetadata {
|
|
||||||
/**
|
|
||||||
* @param {Object} Instance object.
|
|
||||||
*/
|
|
||||||
constructor(data, series, study, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
this.init(series, study);
|
|
||||||
}
|
|
||||||
|
|
||||||
init(series, study) {
|
|
||||||
const instance = this.getData();
|
|
||||||
|
|
||||||
// Initialize Private Properties
|
|
||||||
Object.defineProperties(this, {
|
|
||||||
_sopInstanceUID: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: instance.SOPInstanceUID,
|
|
||||||
},
|
|
||||||
_study: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: study,
|
|
||||||
},
|
|
||||||
_series: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: series,
|
|
||||||
},
|
|
||||||
_instance: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: instance,
|
|
||||||
},
|
|
||||||
_cache: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: Object.create(null),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override
|
|
||||||
getTagValue(tagOrProperty, defaultValue, bypassCache) {
|
|
||||||
// check if this property has been cached...
|
|
||||||
if (tagOrProperty in this._cache && bypassCache !== true) {
|
|
||||||
return this._cache[tagOrProperty];
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceData = this._instance.metadata;
|
|
||||||
|
|
||||||
// Search property value in the whole study metadata chain...
|
|
||||||
let rawValue;
|
|
||||||
if (tagOrProperty in instanceData) {
|
|
||||||
rawValue = instanceData[tagOrProperty];
|
|
||||||
} else if (tagOrProperty in this._series) {
|
|
||||||
rawValue = this._series[tagOrProperty];
|
|
||||||
} else if (tagOrProperty in this._study) {
|
|
||||||
rawValue = this._study[tagOrProperty];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawValue !== void 0) {
|
|
||||||
// if rawValue value is not undefined, cache result...
|
|
||||||
this._cache[tagOrProperty] = rawValue;
|
|
||||||
return rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override
|
|
||||||
tagExists(tagOrProperty) {
|
|
||||||
return (
|
|
||||||
tagOrProperty in this._instance.metadata ||
|
|
||||||
tagOrProperty in this._series ||
|
|
||||||
tagOrProperty in this._study
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override
|
|
||||||
getImageId(frame, thumbnail) {
|
|
||||||
// If _imageID is not cached, create it
|
|
||||||
if (this._imageId === null) {
|
|
||||||
this._imageId = getImageId(this.getData(), frame, thumbnail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._imageId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { SeriesMetadata } from './SeriesMetadata';
|
|
||||||
import { OHIFInstanceMetadata } from './OHIFInstanceMetadata';
|
|
||||||
|
|
||||||
export class OHIFSeriesMetadata extends SeriesMetadata {
|
|
||||||
/**
|
|
||||||
* @param {Object} Series object.
|
|
||||||
*/
|
|
||||||
constructor(data, study, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
this.init(study);
|
|
||||||
}
|
|
||||||
|
|
||||||
init(study) {
|
|
||||||
const series = this.getData();
|
|
||||||
|
|
||||||
// define "_seriesInstanceUID" protected property...
|
|
||||||
Object.defineProperty(this, '_seriesInstanceUID', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: series.SeriesInstanceUID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// populate internal list of instances...
|
|
||||||
series.instances.forEach(instance => {
|
|
||||||
this.addInstance(new OHIFInstanceMetadata(instance, series, study));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { StudyMetadata } from './StudyMetadata';
|
|
||||||
import { OHIFSeriesMetadata } from './OHIFSeriesMetadata';
|
|
||||||
|
|
||||||
export class OHIFStudyMetadata extends StudyMetadata {
|
|
||||||
/**
|
|
||||||
* @param {Object} Study object.
|
|
||||||
*/
|
|
||||||
constructor(data, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
const study = this.getData();
|
|
||||||
|
|
||||||
// define "_studyInstanceUID" protected property
|
|
||||||
Object.defineProperty(this, '_studyInstanceUID', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: study.StudyInstanceUID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// populate internal list of series
|
|
||||||
study.series.forEach(series => {
|
|
||||||
this.addSeries(new OHIFSeriesMetadata(series, study));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# Study Metadata Module
|
|
||||||
|
|
||||||
This module defines the API/Data-Model by which OHIF Viewerbase package and
|
|
||||||
possibly distinct viewer implementations can access studies metadata. This
|
|
||||||
module does not attempt to define any means of _loading_ study metadata from any
|
|
||||||
data end-point but only how the data that has been previously loaded into the
|
|
||||||
application context will be accessed by any of the routines or algorithm
|
|
||||||
implementations that need the data.
|
|
||||||
|
|
||||||
## Intro
|
|
||||||
|
|
||||||
For various reasons like sorting, grouping or simply rendering study
|
|
||||||
information, OHIF Viewerbase package and applications depending on it usually
|
|
||||||
have the need to access study metadata. Before the current initiative there was
|
|
||||||
no uniform way of achieving that since each implementation provides study
|
|
||||||
metadata on its own specific ways. The application and the package itself needed
|
|
||||||
to have a deep knowledge of the data structures provided by the data endpoint to
|
|
||||||
perform any of the operations mentioned above, meaning that any data access code
|
|
||||||
needed to be adapted or rewritten.
|
|
||||||
|
|
||||||
The intent of the current module is to provide a fairly consistent and flexible
|
|
||||||
API/Data-Model by which OHIF Viewerbase package (and different viewer
|
|
||||||
implementations that depend on it) can manipulate DICOM matadata retrieved from
|
|
||||||
distinct data end points (e.g., a proprietary back end servers) in uniform ways
|
|
||||||
with minor to no modifications needed.
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
The current API implementation defines three classes of objects:
|
|
||||||
`StudyMetadata`, `SeriesMetadata` and `InstanceMetadata`. Inside OHIF Viewerbase
|
|
||||||
package, every access to Study, Series or SOP Instance metadata is achieved by
|
|
||||||
the interface exposed by these three classes. By inheriting from them and
|
|
||||||
overriding or extending their methods, different applications with different
|
|
||||||
data models can adapt even the most peculiar data structures to the uniform
|
|
||||||
interface defined by those classes. Together these classes define a flexible and
|
|
||||||
extensible data manipulation layer leaving routines and algorithms that depend
|
|
||||||
on that data untouched.
|
|
||||||
|
|
||||||
## Design Decisions & "_Protected_" Members
|
|
||||||
|
|
||||||
In order to provide for good programming practices, attributes and methods meant
|
|
||||||
to be used exclusively by the classes themselves (for internal purposes only)
|
|
||||||
were written with an initial '\_' character, being thus treated as "_protected_"
|
|
||||||
members. The idea behind this practice was never to hide them from the
|
|
||||||
programmers (what makes debugging tasks painful) but only advise for something
|
|
||||||
that's not part of the official public API and thus should not be relied on.
|
|
||||||
Usage of "protected" members makes the code less readable and prone to
|
|
||||||
compatibility issues.
|
|
||||||
|
|
||||||
As an example, the initial implementation of the `StudyMetadata` class defined
|
|
||||||
the attribute `_studyInstanceUID` and the method `getStudyInstanceUID`. This
|
|
||||||
implies that whenever the _StudyInstanceUID_ of a given study needs to be
|
|
||||||
retrieved the `getStudyInstanceUID` method should be called instead of directly
|
|
||||||
accessing the attribute `_studyInstanceUID` (which might not even be populated
|
|
||||||
since `getStudyInstanceUID` can be possiblity overriden by a subclass to satisfy
|
|
||||||
specific implementation needs, leaving the attribute `_studyInstanceUID`
|
|
||||||
unused).
|
|
||||||
|
|
||||||
Ex:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let studyUID = myStudy.getStudyInstanceUID(); // GOOD! :-)
|
|
||||||
[ ... ]
|
|
||||||
let otherStudyUID = anotherStudy._studyInstanceUID; // BAD... :-(
|
|
||||||
```
|
|
||||||
|
|
||||||
Another important topic is the preference of _methods_ over _attributes_ on the
|
|
||||||
public API. This design decision was made to ensure extensibility and
|
|
||||||
flexibility (methods are extensible while standalone attributes are not, and can
|
|
||||||
be adapted – through overrides, for example – to support even the most peculiar
|
|
||||||
data models) even though the overhead a few additional function calls may incur.
|
|
||||||
|
|
||||||
## Abstract Classes
|
|
||||||
|
|
||||||
Some classes defined in this module are "_abstract_" classes (even though
|
|
||||||
JavaScript does not _officially_ support such programming facility). They are
|
|
||||||
_abstract_ in the sense that a few methods (very important ones, by the way)
|
|
||||||
were left "_blank_" (unimplemented, or more precisely implemented as empty NOP
|
|
||||||
functions) in order to be implemented by specialized subclasses. Methods
|
|
||||||
believed to be more generic were implemented in an attempt to satify most
|
|
||||||
implementation needs but nothing prevents a subclass from overriding them as
|
|
||||||
well (again, flexibility and extensibility are design goals). Most implemented
|
|
||||||
methods rely on the implementation of an unimplemented method. For example, the
|
|
||||||
method `getStringValue` from `InstanceMetadata` class, which has indeed been
|
|
||||||
implemented and is meant to retrieve a metadata value as a string, internally
|
|
||||||
calls the `getTagValue` method which _was NOT implemented_ and is meant to query
|
|
||||||
the internal data structures for the requested metadata value and return it _as
|
|
||||||
is_. Used in that way, an application would not benefit much from the already
|
|
||||||
implemented methods. On the other hand, by simply overriding the `getTagValue`
|
|
||||||
method on a specialized class to deal with the intrinsics of its internal data
|
|
||||||
structures, this very application would now benefit from all already implemented
|
|
||||||
methods.
|
|
||||||
|
|
||||||
The following code snippet tries to illustrate the idea:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
|
|
||||||
// -- InstanceMetadata.js
|
|
||||||
|
|
||||||
class InstanceMetadata {
|
|
||||||
[ ... ]
|
|
||||||
getTagValue(tagOrProperty, defaultValue) {
|
|
||||||
// Please implement this method in a specialized subclass...
|
|
||||||
}
|
|
||||||
[ ... ]
|
|
||||||
getStringValue(tagOrProperty, index, defaultValue) {
|
|
||||||
let rawValue = this.getTagValue(tagOrProperty, '');
|
|
||||||
// parse the returned value into a string...
|
|
||||||
[ ... ]
|
|
||||||
return stringValue;
|
|
||||||
}
|
|
||||||
[ ... ]
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- MyFancyAppInstanceMetadata.js
|
|
||||||
|
|
||||||
class MyFancyAppInstanceMetadata extends InstanceMetadata {
|
|
||||||
// Overriding this method will make all methods implemented in the super class
|
|
||||||
// that rely on it to be immediately available...
|
|
||||||
getTagValue(tagOrProperty, defaultValue) {
|
|
||||||
let tagValue;
|
|
||||||
// retrieve raw value from internal data structures...
|
|
||||||
[ ... ]
|
|
||||||
return tagValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- main.js
|
|
||||||
|
|
||||||
[ ... ]
|
|
||||||
let sopInstaceMetadata = new MyFancyAppInstanceMetadata(myInternalData);
|
|
||||||
if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true
|
|
||||||
// this code will be executed...
|
|
||||||
}
|
|
||||||
if (sopInstaceMetadata instanceof InstanceMetadata) { // also true
|
|
||||||
// this code will also be executed...
|
|
||||||
}
|
|
||||||
// The following will also work since the internal "getTagValue" call inside
|
|
||||||
// "getStringValue" method will now be satisfied... (thanks to the override)
|
|
||||||
let PatientName = sopInstaceMetadata.getStringValue('PatientName', '');
|
|
||||||
[ ... ]
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
_Copyright © 2016 nucleushealth™. All rights reserved_
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
import { Metadata } from './Metadata';
|
|
||||||
import { InstanceMetadata } from './InstanceMetadata';
|
|
||||||
|
|
||||||
export class SeriesMetadata extends Metadata {
|
|
||||||
constructor(data, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
// Initialize Private Properties
|
|
||||||
Object.defineProperties(this, {
|
|
||||||
_seriesInstanceUID: {
|
|
||||||
configurable: true, // configurable so that it can be redefined in sub-classes...
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
_instances: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
_firstInstance: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Initialize Public Properties
|
|
||||||
this._definePublicProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define Public Properties
|
|
||||||
* This method should only be called during initialization (inside the class constructor)
|
|
||||||
*/
|
|
||||||
_definePublicProperties() {
|
|
||||||
/**
|
|
||||||
* Property: this.seriesInstanceUID
|
|
||||||
* Same as this.getSeriesInstanceUID()
|
|
||||||
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
|
|
||||||
* seriesCollection.findBy({
|
|
||||||
* seriesInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
Object.defineProperty(this, 'seriesInstanceUID', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
get: function() {
|
|
||||||
return this.getSeriesInstanceUID();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the SeriesInstanceUID of the current series.
|
|
||||||
*/
|
|
||||||
getSeriesInstanceUID() {
|
|
||||||
return this._seriesInstanceUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append an instance to the current series.
|
|
||||||
* @param {InstanceMetadata} instance The instance to be added to the current series.
|
|
||||||
* @returns {boolean} Returns true on success, false otherwise.
|
|
||||||
*/
|
|
||||||
addInstance(instance) {
|
|
||||||
let result = false;
|
|
||||||
if (
|
|
||||||
instance instanceof InstanceMetadata &&
|
|
||||||
this.getInstanceByUID(instance.getSOPInstanceUID()) === void 0
|
|
||||||
) {
|
|
||||||
this._instances.push(instance);
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first instance of the current series retaining a consistent result across multiple calls.
|
|
||||||
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
|
|
||||||
*/
|
|
||||||
getFirstInstance() {
|
|
||||||
let instance = this._firstInstance;
|
|
||||||
if (!(instance instanceof InstanceMetadata)) {
|
|
||||||
instance = null;
|
|
||||||
const found = this.getInstanceByIndex(0);
|
|
||||||
if (found instanceof InstanceMetadata) {
|
|
||||||
this._firstInstance = found;
|
|
||||||
instance = found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find an instance by index.
|
|
||||||
* @param {number} index An integer representing a list index.
|
|
||||||
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
|
|
||||||
*/
|
|
||||||
getInstanceByIndex(index) {
|
|
||||||
let found; // undefined by default...
|
|
||||||
if (Metadata.isValidIndex(index)) {
|
|
||||||
found = this._instances[index];
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find an instance by SOPInstanceUID.
|
|
||||||
* @param {string} uid An UID string.
|
|
||||||
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
|
|
||||||
*/
|
|
||||||
getInstanceByUID(uid) {
|
|
||||||
let found; // undefined by default...
|
|
||||||
if (Metadata.isValidUID(uid)) {
|
|
||||||
found = this._instances.find(instance => {
|
|
||||||
return instance.getSOPInstanceUID() === uid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of instances within the current series.
|
|
||||||
* @returns {number} The number of instances in the current series.
|
|
||||||
*/
|
|
||||||
getInstanceCount() {
|
|
||||||
return this._instances.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the supplied callback for each instance in the current series passing
|
|
||||||
* two arguments: instance (an InstanceMetadata instance) and index (the integer
|
|
||||||
* index of the instance within the current series)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each instance in the series.
|
|
||||||
* @returns {undefined} Nothing is returned.
|
|
||||||
*/
|
|
||||||
forEachInstance(callback) {
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
this._instances.forEach((instance, index) => {
|
|
||||||
callback.call(null, instance, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the index of an instance inside the series.
|
|
||||||
* @param {InstanceMetadata} instance An instance of the SeriesMetadata class.
|
|
||||||
* @returns {number} The index of the instance inside the series or -1 if not found.
|
|
||||||
*/
|
|
||||||
indexOfInstance(instance) {
|
|
||||||
return this._instances.indexOf(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the associated instances using the supplied callback as criteria. The callback is passed
|
|
||||||
* two arguments: instance (a InstanceMetadata instance) and index (the integer
|
|
||||||
* index of the instance within its series)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each instance.
|
|
||||||
* @returns {InstanceMetadata|undefined} If an instance is found based on callback criteria it
|
|
||||||
* returns a InstanceMetadata. "undefined" is returned otherwise
|
|
||||||
*/
|
|
||||||
findInstance(callback) {
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
return this._instances.find((instance, index) => {
|
|
||||||
return callback.call(null, instance, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares the current series with another one.
|
|
||||||
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
|
|
||||||
* @returns {boolean} Returns true if both instances refer to the same series.
|
|
||||||
*/
|
|
||||||
equals(series) {
|
|
||||||
const self = this;
|
|
||||||
return (
|
|
||||||
series === self ||
|
|
||||||
(series instanceof SeriesMetadata &&
|
|
||||||
series.getSeriesInstanceUID() === self.getSeriesInstanceUID())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,906 +0,0 @@
|
|||||||
// - createStacks
|
|
||||||
import DICOMWeb from './../../DICOMWeb';
|
|
||||||
import ImageSet from './../ImageSet';
|
|
||||||
import { InstanceMetadata } from './InstanceMetadata';
|
|
||||||
import { Metadata } from './Metadata';
|
|
||||||
import OHIFError from '../OHIFError';
|
|
||||||
import { SeriesMetadata } from './SeriesMetadata';
|
|
||||||
// - createStacks
|
|
||||||
import { api } from 'dicomweb-client';
|
|
||||||
// - createStacks
|
|
||||||
import { isImage } from '../../utils/isImage';
|
|
||||||
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
|
||||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
|
||||||
import errorHandler from '../../errorHandler';
|
|
||||||
|
|
||||||
export class StudyMetadata extends Metadata {
|
|
||||||
constructor(data, uid) {
|
|
||||||
super(data, uid);
|
|
||||||
// Initialize Private Properties
|
|
||||||
Object.defineProperties(this, {
|
|
||||||
_studyInstanceUID: {
|
|
||||||
configurable: true, // configurable so that it can be redefined in sub-classes...
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
_series: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
_displaySets: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
_derivedDisplaySets: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: false,
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
_firstSeries: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
_firstInstance: {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
writable: true,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Initialize Public Properties
|
|
||||||
this._definePublicProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define Public Properties
|
|
||||||
* This method should only be called during initialization (inside the class constructor)
|
|
||||||
*/
|
|
||||||
_definePublicProperties() {
|
|
||||||
/**
|
|
||||||
* Property: this.studyInstanceUID
|
|
||||||
* Same as this.getStudyInstanceUID()
|
|
||||||
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
|
|
||||||
* studyCollection.findBy({
|
|
||||||
* studyInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
Object.defineProperty(this, 'studyInstanceUID', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
get: function() {
|
|
||||||
return this.getStudyInstanceUID();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getter for displaySets
|
|
||||||
* @return {Array} Array of display set object
|
|
||||||
*/
|
|
||||||
getDisplaySets() {
|
|
||||||
return this._displaySets.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split a series metadata object into display sets
|
|
||||||
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
|
||||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
|
||||||
* @returns {Array} The list of display sets created for the given series object
|
|
||||||
*/
|
|
||||||
_createDisplaySetsForSeries(sopClassHandlerModules, series) {
|
|
||||||
const study = this;
|
|
||||||
const displaySets = [];
|
|
||||||
|
|
||||||
const anyInstances = series.getInstanceCount() > 0;
|
|
||||||
|
|
||||||
if (!anyInstances) {
|
|
||||||
const displaySet = new ImageSet([]);
|
|
||||||
const seriesData = series.getData();
|
|
||||||
|
|
||||||
displaySet.setAttributes({
|
|
||||||
displaySetInstanceUID: displaySet.uid,
|
|
||||||
SeriesInstanceUID: seriesData.SeriesInstanceUID,
|
|
||||||
SeriesDescription: seriesData.SeriesDescription,
|
|
||||||
SeriesNumber: seriesData.SeriesNumber,
|
|
||||||
Modality: seriesData.Modality,
|
|
||||||
});
|
|
||||||
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopClassUIDs = getSopClassUIDs(series);
|
|
||||||
|
|
||||||
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
|
|
||||||
const displaySet = _getDisplaySetFromSopClassModule(
|
|
||||||
sopClassHandlerModules,
|
|
||||||
series,
|
|
||||||
study,
|
|
||||||
sopClassUIDs
|
|
||||||
);
|
|
||||||
if (displaySet) {
|
|
||||||
displaySet.sopClassModule = true;
|
|
||||||
|
|
||||||
displaySet.isDerived
|
|
||||||
? this._addDerivedDisplaySet(displaySet)
|
|
||||||
: displaySets.push(displaySet);
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
|
||||||
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
|
||||||
|
|
||||||
// Search through the instances (InstanceMetadata object) of this series
|
|
||||||
// Split Multi-frame instances and Single-image modalities
|
|
||||||
// into their own specific display sets. Place the rest of each
|
|
||||||
// series into another display set.
|
|
||||||
const stackableInstances = [];
|
|
||||||
series.forEachInstance(instance => {
|
|
||||||
// All imaging modalities must have a valid value for SOPClassUID (x00080016) or Rows (x00280010)
|
|
||||||
if (
|
|
||||||
!isImage(instance.getTagValue('SOPClassUID')) &&
|
|
||||||
!instance.getTagValue('Rows')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displaySet;
|
|
||||||
|
|
||||||
if (isMultiFrame(instance)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUIDs,
|
|
||||||
isClip: true,
|
|
||||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
|
||||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
|
|
||||||
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
|
|
||||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
|
||||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else if (isSingleImageModality(instance.Modality)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUIDs,
|
|
||||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
|
|
||||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
|
||||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
|
||||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else {
|
|
||||||
stackableInstances.push(instance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stackableInstances.length) {
|
|
||||||
const displaySet = makeDisplaySet(series, stackableInstances);
|
|
||||||
displaySet.setAttribute('StudyInstanceUID', study.getStudyInstanceUID());
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUIDs,
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the displaySets to the studies list of derived displaySets.
|
|
||||||
* @param {object} displaySet The displaySet to append to the derived displaysets list.
|
|
||||||
*/
|
|
||||||
_addDerivedDisplaySet(displaySet) {
|
|
||||||
this._derivedDisplaySets.push(displaySet);
|
|
||||||
// --> Perhaps that logic should exist in the extension sop class handler and this be a dumb list.
|
|
||||||
// TODO -> Get x Modality by referencedSeriesInstanceUid, FoR, etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of derived datasets in the study, filtered by the given filter.
|
|
||||||
* @param {object} filter An object containing search filters
|
|
||||||
* @param {object} filter.Modality
|
|
||||||
* @param {object} filter.referencedSeriesInstanceUID
|
|
||||||
* @param {object} filter.referencedFrameOfReferenceUID
|
|
||||||
* @return {Array} filtered derived display sets
|
|
||||||
*/
|
|
||||||
getDerivedDatasets(filter) {
|
|
||||||
const {
|
|
||||||
Modality,
|
|
||||||
referencedSeriesInstanceUID,
|
|
||||||
referencedFrameOfReferenceUID,
|
|
||||||
} = filter;
|
|
||||||
|
|
||||||
let filteredDerivedDisplaySets = this._derivedDisplaySets;
|
|
||||||
|
|
||||||
if (Modality) {
|
|
||||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
|
||||||
displaySet => displaySet.Modality === Modality
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referencedSeriesInstanceUID) {
|
|
||||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
|
||||||
displaySet => {
|
|
||||||
if (!displaySet.metadata.ReferencedSeriesSequence) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReferencedSeriesSequence = Array.isArray(
|
|
||||||
displaySet.metadata.ReferencedSeriesSequence
|
|
||||||
)
|
|
||||||
? displaySet.metadata.ReferencedSeriesSequence
|
|
||||||
: [displaySet.metadata.ReferencedSeriesSequence];
|
|
||||||
|
|
||||||
return ReferencedSeriesSequence.some(
|
|
||||||
ReferencedSeries =>
|
|
||||||
ReferencedSeries.SeriesInstanceUID === referencedSeriesInstanceUID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referencedFrameOfReferenceUID) {
|
|
||||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
|
||||||
displaySet =>
|
|
||||||
displaySet.ReferencedFrameOfReferenceUID ===
|
|
||||||
ReferencedFrameOfReferenceUID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredDerivedDisplaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a set of displaySets to be placed in the Study Metadata
|
|
||||||
* The displaySets that appear in the Study Metadata must represent
|
|
||||||
* imaging modalities. A series may be split into one or more displaySets.
|
|
||||||
*
|
|
||||||
* Furthermore, for drag/drop functionality,
|
|
||||||
* it is easiest if the stack objects also contain information about
|
|
||||||
* which study they are linked to.
|
|
||||||
*
|
|
||||||
* @param {StudyMetadata} study The study instance metadata to be used
|
|
||||||
* @returns {Array} An array of series to be placed in the Study Metadata
|
|
||||||
*/
|
|
||||||
createDisplaySets(sopClassHandlerModules) {
|
|
||||||
const displaySets = [];
|
|
||||||
const anyDisplaySets = this.getSeriesCount();
|
|
||||||
|
|
||||||
if (!anyDisplaySets) {
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
|
||||||
this.forEachSeries(series => {
|
|
||||||
const displaySetsForSeries = this._createDisplaySetsForSeries(
|
|
||||||
sopClassHandlerModules,
|
|
||||||
series
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySets.push(...displaySetsForSeries);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortDisplaySetList(displaySets);
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDisplaySets() {
|
|
||||||
sortDisplaySetList(this._displaySets);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to append display sets from a given series to the internal list of display sets
|
|
||||||
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
|
|
||||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
|
||||||
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
|
||||||
*/
|
|
||||||
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
|
||||||
if (!this.containsSeries(series)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displaySets = this._createDisplaySetsForSeries(
|
|
||||||
sopClassHandlerModules,
|
|
||||||
series
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: filtering in place because this._displaySets has writable: false
|
|
||||||
for (let i = this._displaySets.length - 1; i >= 0; i--) {
|
|
||||||
const displaySet = this._displaySets[i];
|
|
||||||
if (displaySet.SeriesInstanceUID === series.getSeriesInstanceUID()) {
|
|
||||||
this._displaySets.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
displaySets.forEach(displaySet => {
|
|
||||||
this.addDisplaySet(displaySet);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sortDisplaySets();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set display sets
|
|
||||||
* @param {Array} displaySets Array of display sets (ImageSet[])
|
|
||||||
*/
|
|
||||||
setDisplaySets(displaySets) {
|
|
||||||
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
|
||||||
// TODO: This is weird, can we just switch it to writable: true?
|
|
||||||
this._displaySets.splice(0);
|
|
||||||
|
|
||||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
|
||||||
this.sortDisplaySets();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a single display set to the list
|
|
||||||
* @param {Object} displaySet Display set object
|
|
||||||
* @returns {boolean} True on success, false on failure.
|
|
||||||
*/
|
|
||||||
addDisplaySet(displaySet) {
|
|
||||||
if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
|
|
||||||
this._displaySets.push(displaySet);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the supplied callback for each display set in the current study passing
|
|
||||||
* two arguments: display set (a ImageSet instance) and index (the integer
|
|
||||||
* index of the display set within the current study)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each display set instance.
|
|
||||||
* @returns {undefined} Nothing is returned.
|
|
||||||
*/
|
|
||||||
forEachDisplaySet(callback) {
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
this._displaySets.forEach((displaySet, index) => {
|
|
||||||
callback.call(null, displaySet, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the associated display sets using the supplied callback as criteria. The callback is passed
|
|
||||||
* two arguments: display set (an ImageSet instance) and index (the integer
|
|
||||||
* index of the display set within the current study)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each display set instance.
|
|
||||||
* @returns {undefined} Nothing is returned.
|
|
||||||
*/
|
|
||||||
findDisplaySet(callback) {
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
return this._displaySets.find((displaySet, index) => {
|
|
||||||
return callback.call(null, displaySet, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of display sets within the current study.
|
|
||||||
* @returns {number} The number of display sets in the current study.
|
|
||||||
*/
|
|
||||||
getDisplaySetCount() {
|
|
||||||
return this._displaySets.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the StudyInstanceUID of the current study.
|
|
||||||
*/
|
|
||||||
getStudyInstanceUID() {
|
|
||||||
return this._studyInstanceUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getter for series
|
|
||||||
* @return {Array} Array of SeriesMetadata object
|
|
||||||
*/
|
|
||||||
getSeries() {
|
|
||||||
return this._series.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a series to the current study.
|
|
||||||
* @param {SeriesMetadata} series The series to be added to the current study.
|
|
||||||
* @returns {boolean} Returns true on success, false otherwise.
|
|
||||||
*/
|
|
||||||
addSeries(series) {
|
|
||||||
let result = false;
|
|
||||||
if (
|
|
||||||
series instanceof SeriesMetadata &&
|
|
||||||
this.getSeriesByUID(series.getSeriesInstanceUID()) === void 0
|
|
||||||
) {
|
|
||||||
this._series.push(series);
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a series in the current study by SeriesInstanceUID.
|
|
||||||
* @param {String} SeriesInstanceUID The SeriesInstanceUID to be updated
|
|
||||||
* @param {SeriesMetadata} series The series to be added to the current study.
|
|
||||||
* @returns {boolean} Returns true on success, false otherwise.
|
|
||||||
*/
|
|
||||||
updateSeries(SeriesInstanceUID, series) {
|
|
||||||
const index = this._series.findIndex(series => {
|
|
||||||
return series.getSeriesInstanceUID() === SeriesInstanceUID;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(series instanceof SeriesMetadata)) {
|
|
||||||
throw new Error('Series must be an instance of SeriesMetadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._series[index] = series;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a series by index.
|
|
||||||
* @param {number} index An integer representing a list index.
|
|
||||||
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
|
|
||||||
*/
|
|
||||||
getSeriesByIndex(index) {
|
|
||||||
let found; // undefined by default...
|
|
||||||
if (Metadata.isValidIndex(index)) {
|
|
||||||
found = this._series[index];
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a series by SeriesInstanceUID.
|
|
||||||
* @param {string} uid An UID string.
|
|
||||||
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
|
|
||||||
*/
|
|
||||||
getSeriesByUID(uid) {
|
|
||||||
let found; // undefined by default...
|
|
||||||
if (Metadata.isValidUID(uid)) {
|
|
||||||
found = this._series.find(series => {
|
|
||||||
return series.getSeriesInstanceUID() === uid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
containsSeries(series) {
|
|
||||||
return (
|
|
||||||
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of series within the current study.
|
|
||||||
* @returns {number} The number of series in the current study.
|
|
||||||
*/
|
|
||||||
getSeriesCount() {
|
|
||||||
return this._series.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of instances within the current study.
|
|
||||||
* @returns {number} The number of instances in the current study.
|
|
||||||
*/
|
|
||||||
getInstanceCount() {
|
|
||||||
return this._series.reduce((sum, series) => {
|
|
||||||
return sum + series.getInstanceCount();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the supplied callback for each series in the current study passing
|
|
||||||
* two arguments: series (a SeriesMetadata instance) and index (the integer
|
|
||||||
* index of the series within the current study)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each series instance.
|
|
||||||
* @returns {undefined} Nothing is returned.
|
|
||||||
*/
|
|
||||||
forEachSeries(callback) {
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
this._series.forEach((series, index) => {
|
|
||||||
callback.call(null, series, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the index of a series inside the study.
|
|
||||||
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
|
|
||||||
* @returns {number} The index of the series inside the study or -1 if not found.
|
|
||||||
*/
|
|
||||||
indexOfSeries(series) {
|
|
||||||
return this._series.indexOf(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It sorts the series based on display sets order. Each series must be an instance
|
|
||||||
* of SeriesMetadata and each display sets must be an instance of ImageSet.
|
|
||||||
* Useful example of usage:
|
|
||||||
* Study data provided by backend does not sort series at all and client-side
|
|
||||||
* needs series sorted by the same criteria used for sorting display sets.
|
|
||||||
*/
|
|
||||||
sortSeriesByDisplaySets() {
|
|
||||||
// Object for mapping display sets' index by SeriesInstanceUID
|
|
||||||
const displaySetsMapping = {};
|
|
||||||
|
|
||||||
// Loop through each display set to create the mapping
|
|
||||||
this.forEachDisplaySet((displaySet, index) => {
|
|
||||||
if (!(displaySet instanceof ImageSet)) {
|
|
||||||
throw new OHIFError(
|
|
||||||
`StudyMetadata::sortSeriesByDisplaySets display set at index ${index} is not an instance of ImageSet`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case of multiframe studies, just get the first index occurence
|
|
||||||
if (displaySetsMapping[displaySet.SeriesInstanceUID] === void 0) {
|
|
||||||
displaySetsMapping[displaySet.SeriesInstanceUID] = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clone of actual series
|
|
||||||
const actualSeries = this.getSeries();
|
|
||||||
|
|
||||||
actualSeries.forEach((series, index) => {
|
|
||||||
if (!(series instanceof SeriesMetadata)) {
|
|
||||||
throw new OHIFError(
|
|
||||||
`StudyMetadata::sortSeriesByDisplaySets series at index ${index} is not an instance of SeriesMetadata`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new series index
|
|
||||||
const seriesIndex = displaySetsMapping[series.getSeriesInstanceUID()];
|
|
||||||
|
|
||||||
// Update the series object with the new series position
|
|
||||||
this._series[seriesIndex] = series;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares the current study instance with another one.
|
|
||||||
* @param {StudyMetadata} study An instance of the StudyMetadata class.
|
|
||||||
* @returns {boolean} Returns true if both instances refer to the same study.
|
|
||||||
*/
|
|
||||||
equals(study) {
|
|
||||||
const self = this;
|
|
||||||
return (
|
|
||||||
study === self ||
|
|
||||||
(study instanceof StudyMetadata &&
|
|
||||||
study.getStudyInstanceUID() === self.getStudyInstanceUID())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first series of the current study retaining a consistent result across multiple calls.
|
|
||||||
* @return {SeriesMetadata} An instance of the SeriesMetadata class or null if it does not exist.
|
|
||||||
*/
|
|
||||||
getFirstSeries() {
|
|
||||||
let series = this._firstSeries;
|
|
||||||
if (!(series instanceof SeriesMetadata)) {
|
|
||||||
series = null;
|
|
||||||
const found = this.getSeriesByIndex(0);
|
|
||||||
if (found instanceof SeriesMetadata) {
|
|
||||||
this._firstSeries = found;
|
|
||||||
series = found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return series;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first image id given display instance uid.
|
|
||||||
* @return {string} The image id.
|
|
||||||
*/
|
|
||||||
getFirstImageId(displaySetInstanceUID) {
|
|
||||||
try {
|
|
||||||
const displaySet = this.findDisplaySet(
|
|
||||||
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
|
|
||||||
);
|
|
||||||
return displaySet.images[0].getImageId();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to retrieve image metadata');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first instance of the current study retaining a consistent result across multiple calls.
|
|
||||||
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
|
|
||||||
*/
|
|
||||||
getFirstInstance() {
|
|
||||||
let instance = this._firstInstance;
|
|
||||||
if (!(instance instanceof InstanceMetadata)) {
|
|
||||||
instance = null;
|
|
||||||
const firstSeries = this.getFirstSeries();
|
|
||||||
if (firstSeries instanceof SeriesMetadata) {
|
|
||||||
const found = firstSeries.getFirstInstance();
|
|
||||||
if (found instanceof InstanceMetadata) {
|
|
||||||
this._firstInstance = found;
|
|
||||||
instance = found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the associated series to find an specific instance using the supplied callback as criteria.
|
|
||||||
* The callback is passed two arguments: instance (a InstanceMetadata instance) and index (the integer
|
|
||||||
* index of the instance within the current series)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each instance instance.
|
|
||||||
* @returns {Object} Result object containing series (SeriesMetadata) and instance (InstanceMetadata)
|
|
||||||
* objects or an empty object if not found.
|
|
||||||
*/
|
|
||||||
findSeriesAndInstanceByInstance(callback) {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
if (Metadata.isValidCallback(callback)) {
|
|
||||||
let instance;
|
|
||||||
|
|
||||||
const series = this._series.find(series => {
|
|
||||||
instance = series.findInstance(callback);
|
|
||||||
return instance instanceof InstanceMetadata;
|
|
||||||
});
|
|
||||||
|
|
||||||
// No series found
|
|
||||||
if (series instanceof SeriesMetadata) {
|
|
||||||
result = {
|
|
||||||
series,
|
|
||||||
instance,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find series by instance using the supplied callback as criteria. The callback is passed
|
|
||||||
* two arguments: instance (a InstanceMetadata instance) and index (the integer index of
|
|
||||||
* the instance within its series)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each instance.
|
|
||||||
* @returns {SeriesMetadata|undefined} If a series is found based on callback criteria it
|
|
||||||
* returns a SeriesMetadata. "undefined" is returned otherwise
|
|
||||||
*/
|
|
||||||
findSeriesByInstance(callback) {
|
|
||||||
const result = this.findSeriesAndInstanceByInstance(callback);
|
|
||||||
|
|
||||||
return result.series;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find an instance using the supplied callback as criteria. The callback is passed
|
|
||||||
* two arguments: instance (a InstanceMetadata instance) and index (the integer index of
|
|
||||||
* the instance within its series)
|
|
||||||
* @param {function} callback The callback function which will be invoked for each instance.
|
|
||||||
* @returns {InstanceMetadata|undefined} If an instance is found based on callback criteria it
|
|
||||||
* returns a InstanceMetadata. "undefined" is returned otherwise
|
|
||||||
*/
|
|
||||||
findInstance(callback) {
|
|
||||||
const result = this.findSeriesAndInstanceByInstance(callback);
|
|
||||||
|
|
||||||
return result.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @typedef StudyMetadata
|
|
||||||
* @property {function} getSeriesCount - returns the number of series in the study
|
|
||||||
* @property {function} forEachSeries - function that invokes callback with each series and index
|
|
||||||
* @property {function} getStudyInstanceUID - returns the study's instance UID
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef SeriesMetadata
|
|
||||||
* @property {function} getSeriesInstanceUID - returns the series's instance UID
|
|
||||||
* @property {function} getData - ???
|
|
||||||
* @property {function} forEachInstance - ???
|
|
||||||
*/
|
|
||||||
|
|
||||||
const dwc = api.DICOMwebClient;
|
|
||||||
|
|
||||||
const isMultiFrame = instance => {
|
|
||||||
return instance.getTagValue('NumberOfFrames') > 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeDisplaySet = (series, instances) => {
|
|
||||||
const instance = instances[0];
|
|
||||||
const imageSet = new ImageSet(instances);
|
|
||||||
const seriesData = series.getData();
|
|
||||||
|
|
||||||
// set appropriate attributes to image set...
|
|
||||||
imageSet.setAttributes({
|
|
||||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
|
||||||
SeriesDate: seriesData.SeriesDate,
|
|
||||||
SeriesTime: seriesData.SeriesTime,
|
|
||||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
|
||||||
SeriesNumber: instance.getTagValue('SeriesNumber'),
|
|
||||||
SeriesDescription: instance.getTagValue('SeriesDescription'),
|
|
||||||
numImageFrames: instances.length,
|
|
||||||
frameRate: instance.getTagValue('FrameTime'),
|
|
||||||
Modality: instance.getTagValue('Modality'),
|
|
||||||
isMultiFrame: isMultiFrame(instance),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort the images in this series if needed
|
|
||||||
const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder');
|
|
||||||
if (shallSort) {
|
|
||||||
imageSet.sortBy((a, b) => {
|
|
||||||
// Sort by InstanceNumber (0020,0013)
|
|
||||||
return (
|
|
||||||
(parseInt(a.getTagValue('InstanceNumber', 0)) || 0) -
|
|
||||||
(parseInt(b.getTagValue('InstanceNumber', 0)) || 0)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the first image instance number (after sorted)
|
|
||||||
imageSet.setAttribute(
|
|
||||||
'InstanceNumber',
|
|
||||||
imageSet.getImage(0).getTagValue('InstanceNumber')
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReconstructable = isDisplaySetReconstructable(instances);
|
|
||||||
|
|
||||||
imageSet.isReconstructable = isReconstructable.value;
|
|
||||||
|
|
||||||
if (shallSort && imageSet.isReconstructable) {
|
|
||||||
imageSet.sortByImagePositionPatient();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReconstructable.missingFrames) {
|
|
||||||
// TODO -> This is currently unused, but may be used for reconstructing
|
|
||||||
// Volumes with gaps later on.
|
|
||||||
imageSet.missingFrames = isReconstructable.missingFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageSet;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSingleImageModality = Modality => {
|
|
||||||
return Modality === 'CR' || Modality === 'MG' || Modality === 'DX';
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSopClassUIDs(series) {
|
|
||||||
const uniqueSopClassUIDsInSeries = new Set();
|
|
||||||
series.forEachInstance(instance => {
|
|
||||||
const instanceSopClassUID = instance.getTagValue('SOPClassUID');
|
|
||||||
|
|
||||||
uniqueSopClassUIDsInSeries.add(instanceSopClassUID);
|
|
||||||
});
|
|
||||||
const sopClassUIDs = Array.from(uniqueSopClassUIDsInSeries);
|
|
||||||
|
|
||||||
return sopClassUIDs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {SeriesMetadata} series
|
|
||||||
* @param {StudyMetadata} study
|
|
||||||
* @param {string[]} sopClassUIDs
|
|
||||||
*/
|
|
||||||
function _getDisplaySetFromSopClassModule(
|
|
||||||
sopClassHandlerExtensions, // TODO: Update Usage
|
|
||||||
series,
|
|
||||||
study,
|
|
||||||
sopClassUIDs
|
|
||||||
) {
|
|
||||||
// TODO: For now only use the plugins if all instances have the same SOPClassUID
|
|
||||||
if (sopClassUIDs.length !== 1) {
|
|
||||||
console.warn(
|
|
||||||
'getDisplaySetFromSopClassPlugin: More than one SOPClassUID in the same series is not yet supported.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SOPClassUID = sopClassUIDs[0];
|
|
||||||
const sopClassHandlerModules = sopClassHandlerExtensions.map(extension => {
|
|
||||||
return extension.module;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlersForSopClassUID = sopClassHandlerModules.filter(module => {
|
|
||||||
return module.sopClassUIDs.includes(SOPClassUID);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Sort by something, so we can determine which plugin to use
|
|
||||||
if (!handlersForSopClassUID || !handlersForSopClassUID.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin = handlersForSopClassUID[0];
|
|
||||||
const headers = DICOMWeb.getAuthorizationHeader();
|
|
||||||
const errorInterceptor = errorHandler.getHTTPErrorHandler();
|
|
||||||
const dicomWebClient = new dwc({
|
|
||||||
url: study.getData().wadoRoot,
|
|
||||||
headers,
|
|
||||||
errorInterceptor,
|
|
||||||
});
|
|
||||||
|
|
||||||
let displaySet = plugin.getDisplaySetFromSeries(
|
|
||||||
series,
|
|
||||||
study,
|
|
||||||
dicomWebClient,
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
if (displaySet && !displaySet.Modality) {
|
|
||||||
const instance = series.getFirstInstance();
|
|
||||||
displaySet.Modality = instance.getTagValue('Modality');
|
|
||||||
}
|
|
||||||
return displaySet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort series primarily by Modality (i.e., series with references to other
|
|
||||||
* series like SEG, KO or PR are grouped in the end of the list) and then by
|
|
||||||
* series number:
|
|
||||||
*
|
|
||||||
* --------
|
|
||||||
* | CT #3 |
|
|
||||||
* | CT #4 |
|
|
||||||
* | CT #5 |
|
|
||||||
* --------
|
|
||||||
* | SEG #1 |
|
|
||||||
* | SEG #2 |
|
|
||||||
* --------
|
|
||||||
*
|
|
||||||
* @param {*} a - DisplaySet
|
|
||||||
* @param {*} b - DisplaySet
|
|
||||||
*/
|
|
||||||
|
|
||||||
function seriesSortingCriteria(a, b) {
|
|
||||||
const isLowPriorityA = isLowPriorityModality(a.Modality);
|
|
||||||
const isLowPriorityB = isLowPriorityModality(b.Modality);
|
|
||||||
if (!isLowPriorityA && isLowPriorityB) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (isLowPriorityA && !isLowPriorityB) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return sortBySeriesNumber(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort series by series number. Series with low
|
|
||||||
* @param {*} a - DisplaySet
|
|
||||||
* @param {*} b - DisplaySet
|
|
||||||
*/
|
|
||||||
function sortBySeriesNumber(a, b) {
|
|
||||||
const seriesNumberAIsGreaterOrUndefined =
|
|
||||||
a.SeriesNumber > b.SeriesNumber || (!a.SeriesNumber && b.SeriesNumber);
|
|
||||||
|
|
||||||
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts a list of display set objects
|
|
||||||
* @param {Array} list A list of display sets to be sorted
|
|
||||||
*/
|
|
||||||
function sortDisplaySetList(list) {
|
|
||||||
return list.sort(seriesSortingCriteria);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { InstanceMetadata } from './InstanceMetadata';
|
|
||||||
import { Metadata } from './Metadata';
|
|
||||||
import { OHIFInstanceMetadata } from './OHIFInstanceMetadata';
|
|
||||||
import { OHIFSeriesMetadata } from './OHIFSeriesMetadata';
|
|
||||||
import { OHIFStudyMetadata } from './OHIFStudyMetadata';
|
|
||||||
import { SeriesMetadata } from './SeriesMetadata';
|
|
||||||
import { StudyMetadata } from './StudyMetadata';
|
|
||||||
|
|
||||||
const metadata = {
|
|
||||||
Metadata,
|
|
||||||
StudyMetadata,
|
|
||||||
SeriesMetadata,
|
|
||||||
InstanceMetadata,
|
|
||||||
OHIFStudyMetadata,
|
|
||||||
OHIFSeriesMetadata,
|
|
||||||
OHIFInstanceMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
Metadata,
|
|
||||||
StudyMetadata,
|
|
||||||
SeriesMetadata,
|
|
||||||
InstanceMetadata,
|
|
||||||
OHIFStudyMetadata,
|
|
||||||
OHIFSeriesMetadata,
|
|
||||||
OHIFInstanceMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default metadata;
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import metadataProvider from './classes/MetadataProvider';
|
|
||||||
import {
|
|
||||||
getBoundingBox,
|
|
||||||
pixelToPage,
|
|
||||||
repositionTextBox,
|
|
||||||
} from './lib/cornerstone.js';
|
|
||||||
|
|
||||||
const cornerstone = {
|
|
||||||
metadataProvider,
|
|
||||||
getBoundingBox,
|
|
||||||
pixelToPage,
|
|
||||||
repositionTextBox,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default cornerstone;
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
import OHIFError from '../classes/OHIFError.js';
|
|
||||||
import metadata from '../classes/metadata/';
|
|
||||||
import { validate } from './lib/validate.js';
|
|
||||||
import { CustomAttributeRetrievalCallbacks } from './customAttributes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import Constants
|
|
||||||
*/
|
|
||||||
const { InstanceMetadata } = metadata;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match a Metadata instance against rules using Validate.js for validation.
|
|
||||||
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
|
||||||
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
|
||||||
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
|
||||||
*/
|
|
||||||
const match = (metadataInstance, rules) => {
|
|
||||||
// Make sure the supplied data is valid.
|
|
||||||
if (!(metadataInstance instanceof InstanceMetadata)) {
|
|
||||||
throw new OHIFError(
|
|
||||||
'HPMatcher::match metadataInstance must be an instance of InstanceMetadata'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
format: 'grouped',
|
|
||||||
};
|
|
||||||
|
|
||||||
const details = {
|
|
||||||
passed: [],
|
|
||||||
failed: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let requiredFailed = false;
|
|
||||||
let score = 0;
|
|
||||||
|
|
||||||
rules.forEach(rule => {
|
|
||||||
const attribute = rule.attribute;
|
|
||||||
|
|
||||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
|
||||||
if (CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
|
||||||
const customAttribute = CustomAttributeRetrievalCallbacks[attribute];
|
|
||||||
metadataInstance.setCustomAttribute(
|
|
||||||
attribute,
|
|
||||||
customAttribute.callback(metadataInstance)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the constraint as required by Validate.js
|
|
||||||
const testConstraint = {
|
|
||||||
[attribute]: rule.constraint,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a single attribute object to be validated, since metadataInstance is an
|
|
||||||
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
|
||||||
const attributeValue = metadataInstance.customAttributeExists(attribute)
|
|
||||||
? metadataInstance.getCustomAttribute(attribute)
|
|
||||||
: metadataInstance.getTagValue(attribute);
|
|
||||||
const attributeMap = {
|
|
||||||
[attribute]: attributeValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
|
||||||
let errorMessages;
|
|
||||||
try {
|
|
||||||
errorMessages = validate(attributeMap, testConstraint, [options]);
|
|
||||||
} catch (e) {
|
|
||||||
errorMessages = ['Something went wrong during validation.', e];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errorMessages) {
|
|
||||||
// If no errorMessages were returned, then validation passed.
|
|
||||||
|
|
||||||
// Add the rule's weight to the total score
|
|
||||||
score += parseInt(rule.weight, 10);
|
|
||||||
|
|
||||||
// Log that this rule passed in the matching details object
|
|
||||||
details.passed.push({
|
|
||||||
rule,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If errorMessages were present, then validation failed
|
|
||||||
|
|
||||||
// If the rule that failed validation was Required, then
|
|
||||||
// mark that a required Rule has failed
|
|
||||||
if (rule.required) {
|
|
||||||
requiredFailed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log that this rule failed in the matching details object
|
|
||||||
// and include any error messages
|
|
||||||
details.failed.push({
|
|
||||||
rule,
|
|
||||||
errorMessages,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If a required Rule has failed Validation, set the matching score to zero
|
|
||||||
if (requiredFailed) {
|
|
||||||
score = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
score,
|
|
||||||
details,
|
|
||||||
requiredFailed,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const HPMatcher = {
|
|
||||||
match,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { HPMatcher };
|
|
||||||
@ -1,812 +0,0 @@
|
|||||||
import OHIFError from '../classes/OHIFError.js';
|
|
||||||
import metadata from '../classes/metadata/';
|
|
||||||
import { StudyMetadataSource } from '../classes/StudyMetadataSource.js';
|
|
||||||
import { isImage } from '../utils/isImage.js';
|
|
||||||
import { HPMatcher } from './HPMatcher.js';
|
|
||||||
import { sortByScore } from './lib/sortByScore';
|
|
||||||
import log from '../log.js';
|
|
||||||
import sortBy from '../utils/sortBy.js';
|
|
||||||
import { CustomViewportSettings } from './customViewportSettings';
|
|
||||||
import Protocol from './classes/Protocol';
|
|
||||||
import { ProtocolStore } from './protocolStore/classes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import Constants
|
|
||||||
*/
|
|
||||||
const { StudyMetadata, InstanceMetadata } = metadata;
|
|
||||||
|
|
||||||
// Useful constants
|
|
||||||
const ABSTRACT_PRIOR_VALUE = 'abstractPriorValue';
|
|
||||||
|
|
||||||
export default class ProtocolEngine {
|
|
||||||
matchedProtocols = new Map();
|
|
||||||
matchedProtocolScores = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
* @param {ProtocolStore} protocolStore Protocol Store used to keep track of all hanging protocols
|
|
||||||
* @param {Array} studies Array of study metadata
|
|
||||||
* @param {Map} priorStudies Map of prior studies
|
|
||||||
* @param {Object} studyMetadataSource Instance of StudyMetadataSource (ohif-viewerbase) Object to get study metadata
|
|
||||||
* @param {Object} options
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
protocolStore,
|
|
||||||
studies,
|
|
||||||
priorStudies,
|
|
||||||
studyMetadataSource,
|
|
||||||
options = {}
|
|
||||||
) {
|
|
||||||
// -----------
|
|
||||||
// Type Validations
|
|
||||||
if (!(studyMetadataSource instanceof StudyMetadataSource)) {
|
|
||||||
throw new OHIFError(
|
|
||||||
'ProtocolEngine::constructor studyMetadataSource is not an instance of StudyMetadataSource'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(studies instanceof Array) &&
|
|
||||||
!studies.every(study => study instanceof StudyMetadata)
|
|
||||||
) {
|
|
||||||
throw new OHIFError(
|
|
||||||
"ProtocolEngine::constructor studies is not an array or it's items are not instances of StudyMetadata"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------
|
|
||||||
// Initialization
|
|
||||||
this.protocolStore = protocolStore;
|
|
||||||
this.studies = studies;
|
|
||||||
this.priorStudies = priorStudies instanceof Map ? priorStudies : new Map();
|
|
||||||
this.studyMetadataSource = studyMetadataSource;
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
// Put protocol engine in a known state
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
// Create an array for new stage ids to be stored
|
|
||||||
// while editing a stage
|
|
||||||
this.newStageIds = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the ProtocolEngine to the best match
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
const protocol = this.getBestProtocolMatch();
|
|
||||||
|
|
||||||
this.setHangingProtocol(protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current Stage from the current Protocol and stage index
|
|
||||||
*
|
|
||||||
* @returns {*} The Stage model for the currently displayed Stage
|
|
||||||
*/
|
|
||||||
getCurrentStageModel() {
|
|
||||||
return this.protocol.stages[this.stage];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
|
||||||
* with the given study. The best protocol are orded by score and returned in an array
|
|
||||||
* @param {Object} study StudyMetadata instance object
|
|
||||||
* @return {Array} Array of match objects or an empty array if no match was found
|
|
||||||
* Each match object has the score of the matching and the matched
|
|
||||||
* protocol
|
|
||||||
*/
|
|
||||||
findMatchByStudy(study) {
|
|
||||||
log.trace('ProtocolEngine::findMatchByStudy');
|
|
||||||
|
|
||||||
const matched = [];
|
|
||||||
const studyInstance = study.getFirstInstance();
|
|
||||||
|
|
||||||
// Set custom attribute for study metadata
|
|
||||||
const numberOfAvailablePriors = this.getNumberOfAvailablePriors(
|
|
||||||
study.getObjectID()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.protocolStore.getProtocol().forEach(protocol => {
|
|
||||||
// Clone the protocol's protocolMatchingRules array
|
|
||||||
// We clone it so that we don't accidentally add the
|
|
||||||
// numberOfPriorsReferenced rule to the Protocol itself.
|
|
||||||
let rules = protocol.protocolMatchingRules.slice();
|
|
||||||
if (!rules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the study has the minimun number of priors used by the protocol.
|
|
||||||
const numberOfPriorsReferenced = protocol.getNumberOfPriorsReferenced();
|
|
||||||
if (numberOfPriorsReferenced > numberOfAvailablePriors) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the matcher and get matching details
|
|
||||||
const matchedDetails = HPMatcher.match(studyInstance, rules);
|
|
||||||
const score = matchedDetails.score;
|
|
||||||
|
|
||||||
// The protocol matched some rule, add it to the matched list
|
|
||||||
if (score > 0) {
|
|
||||||
matched.push({
|
|
||||||
score,
|
|
||||||
protocol,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no matches were found, select the default protocol
|
|
||||||
if (!matched.length) {
|
|
||||||
const defaultProtocol = this.protocolStore.getProtocol('defaultProtocol');
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
score: 1,
|
|
||||||
protocol: defaultProtocol,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the matched list by score
|
|
||||||
sortByScore(matched);
|
|
||||||
|
|
||||||
log.trace('ProtocolEngine::findMatchByStudy matched', matched);
|
|
||||||
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearMatchedProtocols() {
|
|
||||||
this.matchedProtocols.clear();
|
|
||||||
this.matchedProtocolScores = {};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Populates the MatchedProtocols Collection by running the matching procedure
|
|
||||||
*/
|
|
||||||
updateProtocolMatches() {
|
|
||||||
log.trace('ProtocolEngine::updateProtocolMatches');
|
|
||||||
|
|
||||||
// Clear all data currently in matchedProtocols
|
|
||||||
this._clearMatchedProtocols();
|
|
||||||
|
|
||||||
// For each study, find the matching protocols
|
|
||||||
this.studies.forEach(study => {
|
|
||||||
const matched = this.findMatchByStudy(study);
|
|
||||||
|
|
||||||
// For each matched protocol, check if it is already in MatchedProtocols
|
|
||||||
matched.forEach(matchedDetail => {
|
|
||||||
const protocol = matchedDetail.protocol;
|
|
||||||
if (!protocol) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it is not already in the MatchedProtocols Collection, insert it with its score
|
|
||||||
if (!this.matchedProtocols.has(protocol.id)) {
|
|
||||||
log.trace(
|
|
||||||
'ProtocolEngine::updateProtocolMatches inserting protocol match',
|
|
||||||
matchedDetail
|
|
||||||
);
|
|
||||||
this.matchedProtocols.set(protocol.id, protocol);
|
|
||||||
this.matchedProtocolScores[protocol.id] = matchedDetail.score;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_largestKeyByValue(obj) {
|
|
||||||
return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
|
|
||||||
}
|
|
||||||
|
|
||||||
_getHighestScoringProtocol() {
|
|
||||||
if (!Object.keys(this.matchedProtocolScores).length) {
|
|
||||||
return this.protocolStore.getProtocol('defaultProtocol');
|
|
||||||
}
|
|
||||||
const highestScoringProtocolId = this._largestKeyByValue(
|
|
||||||
this.matchedProtocolScores
|
|
||||||
);
|
|
||||||
return this.matchedProtocols.get(highestScoringProtocolId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the best matched Protocol to the current study or set of studies
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getBestProtocolMatch() {
|
|
||||||
// Run the matching to populate matchedProtocols Set and Map
|
|
||||||
this.updateProtocolMatches();
|
|
||||||
|
|
||||||
// Retrieve the highest scoring Protocol
|
|
||||||
const bestMatch = this._getHighestScoringProtocol();
|
|
||||||
|
|
||||||
log.trace('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
|
|
||||||
|
|
||||||
return bestMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of prior studies supplied in the priorStudies map property.
|
|
||||||
*
|
|
||||||
* @param {String} studyObjectID The study object ID of the study whose priors are needed
|
|
||||||
* @returns {number} The number of available prior studies with the same PatientID
|
|
||||||
*/
|
|
||||||
getNumberOfAvailablePriors(studyObjectID) {
|
|
||||||
return this.getAvailableStudyPriors(studyObjectID).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the array of prior studies from a specific study.
|
|
||||||
*
|
|
||||||
* @param {String} studyObjectID The study object ID of the study whose priors are needed
|
|
||||||
* @returns {Array} The array of available priors or an empty array
|
|
||||||
*/
|
|
||||||
getAvailableStudyPriors(studyObjectID) {
|
|
||||||
const priors = this.priorStudies.get(studyObjectID);
|
|
||||||
|
|
||||||
return priors instanceof Array ? priors : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
|
||||||
matchImages(viewport, viewportIndex) {
|
|
||||||
log.trace('ProtocolEngine::matchImages');
|
|
||||||
|
|
||||||
const {
|
|
||||||
studyMatchingRules,
|
|
||||||
seriesMatchingRules,
|
|
||||||
imageMatchingRules: instanceMatchingRules,
|
|
||||||
} = viewport;
|
|
||||||
|
|
||||||
const matchingScores = [];
|
|
||||||
const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
|
|
||||||
const firstInstance = currentStudy.getFirstInstance();
|
|
||||||
|
|
||||||
let highestStudyMatchingScore = 0;
|
|
||||||
let highestSeriesMatchingScore = 0;
|
|
||||||
|
|
||||||
// Set custom attribute for study metadata and it's first instance
|
|
||||||
currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
|
||||||
if (firstInstance instanceof InstanceMetadata) {
|
|
||||||
firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only used if study matching rules has abstract prior values defined...
|
|
||||||
let priorStudies;
|
|
||||||
|
|
||||||
studyMatchingRules.forEach(rule => {
|
|
||||||
if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
|
|
||||||
const validatorType = Object.keys(rule.constraint)[0];
|
|
||||||
const validator = Object.keys(rule.constraint[validatorType])[0];
|
|
||||||
|
|
||||||
let abstractPriorValue = rule.constraint[validatorType][validator];
|
|
||||||
abstractPriorValue = parseInt(abstractPriorValue, 10);
|
|
||||||
// TODO: Restrict or clarify validators for abstractPriorValue?
|
|
||||||
|
|
||||||
// No need to call it more than once...
|
|
||||||
if (!priorStudies) {
|
|
||||||
priorStudies = this.getAvailableStudyPriors(
|
|
||||||
currentStudy.getObjectID()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Revisit this later: What about two studies with the same
|
|
||||||
// study date?
|
|
||||||
|
|
||||||
let priorStudy;
|
|
||||||
if (abstractPriorValue === -1) {
|
|
||||||
priorStudy = priorStudies[priorStudies.length - 1];
|
|
||||||
} else {
|
|
||||||
const studyIndex = Math.max(abstractPriorValue - 1, 0);
|
|
||||||
priorStudy = priorStudies[studyIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid data
|
|
||||||
if (!priorStudy instanceof StudyMetadata) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const priorStudyObjectID = priorStudy.getObjectID();
|
|
||||||
|
|
||||||
// Check if study metadata is already in studies list
|
|
||||||
if (
|
|
||||||
this.studies.find(study => study.getObjectID() === priorStudyObjectID)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
|
|
||||||
this.studyMetadataSource.loadStudy(priorStudy).then(
|
|
||||||
studyMetadata => {
|
|
||||||
// Set the custom attribute abstractPriorValue for the study metadata
|
|
||||||
studyMetadata.setCustomAttribute(
|
|
||||||
ABSTRACT_PRIOR_VALUE,
|
|
||||||
abstractPriorValue
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also add custom attribute
|
|
||||||
const firstInstance = studyMetadata.getFirstInstance();
|
|
||||||
if (firstInstance instanceof InstanceMetadata) {
|
|
||||||
firstInstance.setCustomAttribute(
|
|
||||||
ABSTRACT_PRIOR_VALUE,
|
|
||||||
abstractPriorValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert the new study metadata
|
|
||||||
this.studies.push(studyMetadata);
|
|
||||||
|
|
||||||
// Update the viewport to refresh layout manager with new study
|
|
||||||
this.updateViewports(viewportIndex);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
log.warn(error);
|
|
||||||
throw new OHIFError(
|
|
||||||
`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// TODO: Add relative Date / time
|
|
||||||
});
|
|
||||||
|
|
||||||
this.studies.forEach(study => {
|
|
||||||
const studyMatchDetails = HPMatcher.match(
|
|
||||||
study.getFirstInstance(),
|
|
||||||
studyMatchingRules
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
if (
|
|
||||||
studyMatchDetails.requiredFailed === true ||
|
|
||||||
studyMatchDetails.score < highestStudyMatchingScore
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
highestStudyMatchingScore = studyMatchDetails.score;
|
|
||||||
|
|
||||||
study.forEachSeries(series => {
|
|
||||||
const seriesMatchDetails = HPMatcher.match(
|
|
||||||
series.getFirstInstance(),
|
|
||||||
seriesMatchingRules
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
if (
|
|
||||||
seriesMatchDetails.requiredFailed === true ||
|
|
||||||
seriesMatchDetails.score < highestSeriesMatchingScore
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
|
||||||
|
|
||||||
series.forEachInstance((instance, index) => {
|
|
||||||
// This tests to make sure there is actually image data in this instance
|
|
||||||
// TODO: Change this when we add PDF and MPEG support
|
|
||||||
// See https://ohiforg.atlassian.net/browse/LT-227
|
|
||||||
if (
|
|
||||||
!isImage(instance.getTagValue('SOPClassUID')) &&
|
|
||||||
!instance.getTagValue('Rows')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceMatchDetails = HPMatcher.match(
|
|
||||||
instance,
|
|
||||||
instanceMatchingRules
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
if (instanceMatchDetails.requiredFailed === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchDetails = {
|
|
||||||
passed: [],
|
|
||||||
failed: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
instanceMatchDetails.details.passed
|
|
||||||
);
|
|
||||||
matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
seriesMatchDetails.details.passed
|
|
||||||
);
|
|
||||||
matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
studyMatchDetails.details.passed
|
|
||||||
);
|
|
||||||
|
|
||||||
matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
instanceMatchDetails.details.failed
|
|
||||||
);
|
|
||||||
matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
seriesMatchDetails.details.failed
|
|
||||||
);
|
|
||||||
matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
studyMatchDetails.details.failed
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalMatchScore =
|
|
||||||
instanceMatchDetails.score +
|
|
||||||
seriesMatchDetails.score +
|
|
||||||
studyMatchDetails.score;
|
|
||||||
const currentSOPInstanceUID = instance.getSOPInstanceUID();
|
|
||||||
|
|
||||||
const imageDetails = {
|
|
||||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
|
||||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
|
||||||
SOPInstanceUID: currentSOPInstanceUID,
|
|
||||||
currentImageIdIndex: index,
|
|
||||||
matchingScore: totalMatchScore,
|
|
||||||
matchDetails: matchDetails,
|
|
||||||
sortingInfo: {
|
|
||||||
score: totalMatchScore,
|
|
||||||
study:
|
|
||||||
instance.getTagValue('StudyDate') +
|
|
||||||
instance.getTagValue('StudyTime'),
|
|
||||||
series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
|
|
||||||
instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the displaySet
|
|
||||||
const displaySet = study.findDisplaySet(displaySet =>
|
|
||||||
displaySet.images.find(
|
|
||||||
image => image.getSOPInstanceUID() === currentSOPInstanceUID
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the instance was found, set the displaySet ID
|
|
||||||
if (displaySet) {
|
|
||||||
imageDetails.displaySetInstanceUID = displaySet.getUID();
|
|
||||||
imageDetails.imageId = instance.getImageId();
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingScores.push(imageDetails);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort the matchingScores
|
|
||||||
const sortingFunction = sortBy(
|
|
||||||
{
|
|
||||||
name: 'score',
|
|
||||||
reverse: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'study',
|
|
||||||
reverse: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'series',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
matchingScores.sort((a, b) =>
|
|
||||||
sortingFunction(a.sortingInfo, b.sortingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const bestMatch = matchingScores[0];
|
|
||||||
|
|
||||||
log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
|
|
||||||
|
|
||||||
return {
|
|
||||||
bestMatch,
|
|
||||||
matchingScores,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current layout
|
|
||||||
*
|
|
||||||
* @param {number} numRows
|
|
||||||
* @param {number} numColumns
|
|
||||||
*/
|
|
||||||
setLayout(numRows, numColumns) {
|
|
||||||
if (numRows < 1 && numColumns < 1) {
|
|
||||||
log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.options.setLayout !== 'function') {
|
|
||||||
log.error('Hanging Protocol Engine setLayout callback is not defined');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let viewports = [];
|
|
||||||
const numViewports = numRows * numColumns;
|
|
||||||
|
|
||||||
for (let i = 0; i < numViewports; i++) {
|
|
||||||
viewports.push({});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.setLayout({ numRows, numColumns, viewports });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rerenders viewports that are part of the current layout manager
|
|
||||||
* using the matching rules internal to each viewport.
|
|
||||||
*
|
|
||||||
* If this function is provided the index of a viewport, only the specified viewport
|
|
||||||
* is rerendered.
|
|
||||||
*
|
|
||||||
* @param viewportIndex
|
|
||||||
*/
|
|
||||||
updateViewports(viewportIndex) {
|
|
||||||
log.trace(
|
|
||||||
`ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure we have an active protocol with a non-empty array of display sets
|
|
||||||
if (!this.getNumProtocolStages()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the current stage
|
|
||||||
const stageModel = this.getCurrentStageModel();
|
|
||||||
|
|
||||||
// If the current stage does not fulfill the requirements to be displayed,
|
|
||||||
// stop here.
|
|
||||||
if (
|
|
||||||
!stageModel ||
|
|
||||||
!stageModel.viewportStructure ||
|
|
||||||
!stageModel.viewports ||
|
|
||||||
!stageModel.viewports.length
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the layoutTemplate associated with the current display set's viewport structure
|
|
||||||
// If no such template name exists, stop here.
|
|
||||||
const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
|
||||||
if (!layoutTemplateName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the properties associated with the current display set's viewport structure template
|
|
||||||
// If no such layout properties exist, stop here.
|
|
||||||
const layoutProps = stageModel.viewportStructure.properties;
|
|
||||||
if (!layoutProps) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an empty array to store the output viewportData
|
|
||||||
const viewportData = [];
|
|
||||||
|
|
||||||
// Empty the matchDetails associated with the ProtocolEngine.
|
|
||||||
// This will be used to store the pass/fail details and score
|
|
||||||
// for each of the viewport matching procedures
|
|
||||||
this.matchDetails = [];
|
|
||||||
|
|
||||||
// Loop through each viewport
|
|
||||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
|
||||||
const details = this.matchImages(viewport, viewportIndex);
|
|
||||||
|
|
||||||
this.matchDetails[viewportIndex] = details;
|
|
||||||
|
|
||||||
// Convert any YES/NO values into true/false for Cornerstone
|
|
||||||
const cornerstoneViewportParams = {};
|
|
||||||
|
|
||||||
// Cache viewportSettings keys
|
|
||||||
const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
|
|
||||||
|
|
||||||
viewportSettingsKeys.forEach(key => {
|
|
||||||
let value = viewport.viewportSettings[key];
|
|
||||||
if (value === 'YES') {
|
|
||||||
value = true;
|
|
||||||
} else if (value === 'NO') {
|
|
||||||
value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerstoneViewportParams[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// imageViewerViewports occasionally needs relevant layout data in order to set
|
|
||||||
// the element style of the viewport in question
|
|
||||||
const currentViewportData = {
|
|
||||||
viewportIndex,
|
|
||||||
viewport: cornerstoneViewportParams,
|
|
||||||
...layoutProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
const customSettings = [];
|
|
||||||
viewportSettingsKeys.forEach(id => {
|
|
||||||
const setting = CustomViewportSettings[id];
|
|
||||||
if (!setting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
customSettings.push({
|
|
||||||
id: id,
|
|
||||||
value: viewport.viewportSettings[id],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
currentViewportData.renderedCallback = element => {
|
|
||||||
//console.log('renderedCallback for ' + element.id);
|
|
||||||
customSettings.forEach(customSetting => {
|
|
||||||
log.trace(
|
|
||||||
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
|
||||||
);
|
|
||||||
log.trace(
|
|
||||||
`ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const setting = CustomViewportSettings[customSetting.id];
|
|
||||||
setting.callback(element, customSetting.value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let currentMatch = details.bestMatch;
|
|
||||||
let currentPosition = 1;
|
|
||||||
const scoresLength = details.matchingScores.length;
|
|
||||||
while (
|
|
||||||
currentPosition < scoresLength &&
|
|
||||||
viewportData.find(a => a.imageId === currentMatch.imageId)
|
|
||||||
) {
|
|
||||||
currentMatch = details.matchingScores[currentPosition];
|
|
||||||
currentPosition++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMatch && currentMatch.imageId) {
|
|
||||||
currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
|
|
||||||
currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
|
|
||||||
currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
|
|
||||||
currentViewportData.currentImageIdIndex =
|
|
||||||
currentMatch.currentImageIdIndex;
|
|
||||||
currentViewportData.displaySetInstanceUID =
|
|
||||||
currentMatch.displaySetInstanceUID;
|
|
||||||
currentViewportData.imageId = currentMatch.imageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
|
|
||||||
// if (!currentViewportData.displaySetInstanceUID) {
|
|
||||||
// throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
|
|
||||||
// }
|
|
||||||
|
|
||||||
viewportData.push(currentViewportData);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setLayout(layoutProps.Rows, layoutProps.Columns);
|
|
||||||
|
|
||||||
if (typeof this.options.setViewportSpecificData !== 'function') {
|
|
||||||
log.error(
|
|
||||||
'Hanging Protocol Engine setViewportSpecificData callback is not defined'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If viewportIndex is defined, then update only that viewport
|
|
||||||
if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
|
||||||
this.options.setViewportSpecificData(
|
|
||||||
viewportIndex,
|
|
||||||
viewportData[viewportIndex]
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all viewports
|
|
||||||
viewportData.forEach(viewportSpecificData => {
|
|
||||||
this.options.setViewportSpecificData(
|
|
||||||
viewportSpecificData.viewportIndex,
|
|
||||||
viewportSpecificData
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current Hanging Protocol to the specified Protocol
|
|
||||||
* An optional argument can also be used to prevent the updating of the Viewports
|
|
||||||
*
|
|
||||||
* @param newProtocol
|
|
||||||
* @param updateViewports
|
|
||||||
*/
|
|
||||||
setHangingProtocol(newProtocol, updateViewports = true) {
|
|
||||||
log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
|
|
||||||
log.trace(
|
|
||||||
`ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset the array of newStageIds
|
|
||||||
this.newStageIds = [];
|
|
||||||
|
|
||||||
if (Protocol.prototype.isPrototypeOf(newProtocol)) {
|
|
||||||
this.protocol = newProtocol;
|
|
||||||
} else {
|
|
||||||
this.protocol = new Protocol();
|
|
||||||
this.protocol.fromObject(newProtocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stage = 0;
|
|
||||||
|
|
||||||
// Update viewports by default
|
|
||||||
if (updateViewports) {
|
|
||||||
this.updateViewports();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the next stage is available
|
|
||||||
* @return {Boolean} True if next stage is available or false otherwise
|
|
||||||
*/
|
|
||||||
isNextStageAvailable() {
|
|
||||||
const numberOfStages = this.getNumProtocolStages();
|
|
||||||
|
|
||||||
return this.stage + 1 < numberOfStages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the previous stage is available
|
|
||||||
* @return {Boolean} True if previous stage is available or false otherwise
|
|
||||||
*/
|
|
||||||
isPreviousStageAvailable() {
|
|
||||||
return this.stage - 1 >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the current stage to a new stage index in the display set sequence.
|
|
||||||
* It checks if the next stage exists.
|
|
||||||
*
|
|
||||||
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
|
||||||
* @return {Boolean} True if new stage has set or false, otherwise
|
|
||||||
*/
|
|
||||||
setCurrentProtocolStage(stageAction) {
|
|
||||||
// Check if previous or next stage is available
|
|
||||||
if (stageAction === -1 && !this.isPreviousStageAvailable()) {
|
|
||||||
return false;
|
|
||||||
} else if (stageAction === 1 && !this.isNextStageAvailable()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the new stage
|
|
||||||
this.stage += stageAction;
|
|
||||||
|
|
||||||
// Log the new stage
|
|
||||||
log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
|
||||||
|
|
||||||
// Since stage has changed, we need to update the viewports
|
|
||||||
// and redo matchings
|
|
||||||
this.updateViewports();
|
|
||||||
|
|
||||||
// Everything went well
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the number of Stages in the current Protocol or
|
|
||||||
* undefined if no protocol or stages are set
|
|
||||||
*/
|
|
||||||
getNumProtocolStages() {
|
|
||||||
if (
|
|
||||||
!this.protocol ||
|
|
||||||
!this.protocol.stages ||
|
|
||||||
!this.protocol.stages.length
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.protocol.stages.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches to the next protocol stage in the display set sequence
|
|
||||||
*/
|
|
||||||
nextProtocolStage() {
|
|
||||||
log.trace('ProtocolEngine::nextProtocolStage');
|
|
||||||
|
|
||||||
if (!this.setCurrentProtocolStage(1)) {
|
|
||||||
log.trace('ProtocolEngine::nextProtocolStage failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches to the previous protocol stage in the display set sequence
|
|
||||||
*/
|
|
||||||
previousProtocolStage() {
|
|
||||||
log.trace('ProtocolEngine::previousProtocolStage');
|
|
||||||
|
|
||||||
if (!this.setCurrentProtocolStage(-1)) {
|
|
||||||
log.trace('ProtocolEngine::previousProtocolStage failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
import { ProtocolMatchingRule } from './rules';
|
|
||||||
import { removeFromArray } from '../lib/removeFromArray';
|
|
||||||
import Stage from './Stage';
|
|
||||||
import guid from '../../utils/guid';
|
|
||||||
import user from '../../user';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class represents a Hanging Protocol at the highest level
|
|
||||||
*
|
|
||||||
* @type {Protocol}
|
|
||||||
*/
|
|
||||||
export default class Protocol {
|
|
||||||
/**
|
|
||||||
* The Constructor for the Class to create a Protocol with the bare
|
|
||||||
* minimum information
|
|
||||||
*
|
|
||||||
* @param name The desired name for the Protocol
|
|
||||||
*/
|
|
||||||
constructor(name) {
|
|
||||||
// Create a new UUID for this Protocol
|
|
||||||
this.id = guid();
|
|
||||||
|
|
||||||
// Store a value which determines whether or not a Protocol is locked
|
|
||||||
// This is probably temporary, since we will eventually have role / user
|
|
||||||
// checks for editing. For now we just need it to prevent changes to the
|
|
||||||
// default protocols.
|
|
||||||
this.locked = false;
|
|
||||||
|
|
||||||
// Boolean value to indicate if the protocol has updated priors information
|
|
||||||
// it's set in "updateNumberOfPriorsReferenced" function
|
|
||||||
this.hasUpdatedPriorsInformation = false;
|
|
||||||
|
|
||||||
// Apply the desired name
|
|
||||||
this.name = name;
|
|
||||||
|
|
||||||
// Set the created and modified dates to Now
|
|
||||||
this.createdDate = new Date();
|
|
||||||
this.modifiedDate = new Date();
|
|
||||||
|
|
||||||
// If we are logged in while creating this Protocol,
|
|
||||||
// store this information as well
|
|
||||||
if (user.userLoggedIn && user.userLoggedIn()) {
|
|
||||||
this.createdBy = user.getUserId();
|
|
||||||
this.modifiedBy = user.getUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create two empty Sets specifying which roles
|
|
||||||
// have read and write access to this Protocol
|
|
||||||
this.availableTo = new Set();
|
|
||||||
this.editableBy = new Set();
|
|
||||||
|
|
||||||
// Define empty arrays for the Protocol matching rules
|
|
||||||
// and Stages
|
|
||||||
this.protocolMatchingRules = [];
|
|
||||||
this.stages = [];
|
|
||||||
|
|
||||||
// Define auxiliary values for priors
|
|
||||||
this.numberOfPriorsReferenced = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNumberOfPriorsReferenced(skipCache = false) {
|
|
||||||
let numberOfPriorsReferenced =
|
|
||||||
skipCache !== true ? this.numberOfPriorsReferenced : -1;
|
|
||||||
|
|
||||||
// Check if information is cached already
|
|
||||||
if (numberOfPriorsReferenced > -1) {
|
|
||||||
return numberOfPriorsReferenced;
|
|
||||||
}
|
|
||||||
|
|
||||||
numberOfPriorsReferenced = 0;
|
|
||||||
|
|
||||||
// Search each study matching rule for prior rules
|
|
||||||
// Each stage can have many viewports that can have
|
|
||||||
// multiple study matching rules.
|
|
||||||
this.stages.forEach(stage => {
|
|
||||||
if (!stage.viewports) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.viewports.forEach(viewport => {
|
|
||||||
if (!viewport.studyMatchingRules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport.studyMatchingRules.forEach(rule => {
|
|
||||||
// If the current rule is not a priors rule, it will return -1 then numberOfPriorsReferenced will continue to be 0
|
|
||||||
const priorsReferenced = rule.getNumberOfPriorsReferenced();
|
|
||||||
if (priorsReferenced > numberOfPriorsReferenced) {
|
|
||||||
numberOfPriorsReferenced = priorsReferenced;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.numberOfPriorsReferenced = numberOfPriorsReferenced;
|
|
||||||
|
|
||||||
return numberOfPriorsReferenced;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNumberOfPriorsReferenced() {
|
|
||||||
this.getNumberOfPriorsReferenced(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to update the modifiedDate when the Protocol
|
|
||||||
* has been changed
|
|
||||||
*/
|
|
||||||
protocolWasModified() {
|
|
||||||
// If we are logged in while modifying this Protocol,
|
|
||||||
// store this information as well
|
|
||||||
if (user.userLoggedIn && user.userLoggedIn()) {
|
|
||||||
this.modifiedBy = user.getUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protocol has been modified, so mark priors information
|
|
||||||
// as "outdated"
|
|
||||||
this.hasUpdatedPriorsInformation = false;
|
|
||||||
|
|
||||||
// Update number of priors referenced info
|
|
||||||
this.updateNumberOfPriorsReferenced();
|
|
||||||
|
|
||||||
// Update the modifiedDate with the current Date/Time
|
|
||||||
this.modifiedDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occasionally the Protocol class needs to be instantiated from a JavaScript Object
|
|
||||||
* containing the Protocol data. This function fills in a Protocol with the Object
|
|
||||||
* data.
|
|
||||||
*
|
|
||||||
* @param input A Protocol as a JavaScript Object, e.g. retrieved from JSON
|
|
||||||
*/
|
|
||||||
fromObject(input) {
|
|
||||||
// Check if the input already has an ID
|
|
||||||
// If so, keep it. It not, create a new UUID
|
|
||||||
this.id = input.id || guid();
|
|
||||||
|
|
||||||
// Assign the input name to the Protocol
|
|
||||||
this.name = input.name;
|
|
||||||
|
|
||||||
// Retrieve locked status, use !! to make it truthy
|
|
||||||
// so that undefined values will be set to false
|
|
||||||
this.locked = !!input.locked;
|
|
||||||
|
|
||||||
// TODO: Check how to regenerate Set from Object
|
|
||||||
//this.availableTo = new Set(input.availableTo);
|
|
||||||
//this.editableBy = new Set(input.editableBy);
|
|
||||||
|
|
||||||
// If the input contains Protocol matching rules
|
|
||||||
if (input.protocolMatchingRules) {
|
|
||||||
input.protocolMatchingRules.forEach(ruleObject => {
|
|
||||||
// Create new Rules from the stored data
|
|
||||||
var rule = new ProtocolMatchingRule();
|
|
||||||
rule.fromObject(ruleObject);
|
|
||||||
|
|
||||||
// Add them to the Protocol
|
|
||||||
this.protocolMatchingRules.push(rule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the input contains data for various Stages in the
|
|
||||||
// display set sequence
|
|
||||||
if (input.stages) {
|
|
||||||
input.stages.forEach(stageObject => {
|
|
||||||
// Create Stages from the stored data
|
|
||||||
var stage = new Stage();
|
|
||||||
stage.fromObject(stageObject);
|
|
||||||
|
|
||||||
// Add them to the Protocol
|
|
||||||
this.stages.push(stage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a clone of the current Protocol with a new name
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @returns {Protocol|*}
|
|
||||||
*/
|
|
||||||
createClone(name) {
|
|
||||||
// Create a new JavaScript independent of the current Protocol
|
|
||||||
var currentProtocol = Object.assign({}, this);
|
|
||||||
|
|
||||||
// Create a new Protocol to return
|
|
||||||
var clonedProtocol = new Protocol();
|
|
||||||
|
|
||||||
// Apply the desired properties
|
|
||||||
currentProtocol.id = clonedProtocol.id;
|
|
||||||
clonedProtocol.fromObject(currentProtocol);
|
|
||||||
|
|
||||||
// If we have specified a name, assign it
|
|
||||||
if (name) {
|
|
||||||
clonedProtocol.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the clone
|
|
||||||
clonedProtocol.locked = false;
|
|
||||||
|
|
||||||
// Return the cloned Protocol
|
|
||||||
return clonedProtocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a Stage to this Protocol's display set sequence
|
|
||||||
*
|
|
||||||
* @param stage
|
|
||||||
*/
|
|
||||||
addStage(stage) {
|
|
||||||
this.stages.push(stage);
|
|
||||||
|
|
||||||
// Update the modifiedDate and User that last
|
|
||||||
// modified this Protocol
|
|
||||||
this.protocolWasModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a Rule to this Protocol's array of matching rules
|
|
||||||
*
|
|
||||||
* @param rule
|
|
||||||
*/
|
|
||||||
addProtocolMatchingRule(rule) {
|
|
||||||
this.protocolMatchingRules.push(rule);
|
|
||||||
|
|
||||||
// Update the modifiedDate and User that last
|
|
||||||
// modified this Protocol
|
|
||||||
this.protocolWasModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a Rule from this Protocol's array of matching rules
|
|
||||||
*
|
|
||||||
* @param rule
|
|
||||||
*/
|
|
||||||
removeProtocolMatchingRule(rule) {
|
|
||||||
var wasRemoved = removeFromArray(this.protocolMatchingRules, rule);
|
|
||||||
|
|
||||||
// Update the modifiedDate and User that last
|
|
||||||
// modified this Protocol
|
|
||||||
if (wasRemoved) {
|
|
||||||
this.protocolWasModified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
import { comparators } from '../lib/comparators';
|
|
||||||
import guid from '../../utils/guid';
|
|
||||||
|
|
||||||
const EQUALS_REGEXP = /^equals$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This Class represents a Rule to be evaluated given a set of attributes
|
|
||||||
* Rules have:
|
|
||||||
* - An attribute (e.g. 'SeriesDescription')
|
|
||||||
* - A constraint Object, in the form required by Validate.js:
|
|
||||||
*
|
|
||||||
* rule.constraint = {
|
|
||||||
* contains: {
|
|
||||||
* value: 'T-1'
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* Note: In this example we use the 'contains' Validator, which is a custom Validator defined in Viewerbase
|
|
||||||
*
|
|
||||||
* - A value for whether or not they are Required to be matched (default: False)
|
|
||||||
* - A value for their relative weighting during Protocol or Image matching (default: 1)
|
|
||||||
*/
|
|
||||||
export default class Rule {
|
|
||||||
/**
|
|
||||||
* The Constructor for the Class to create a Rule with the bare
|
|
||||||
* minimum information
|
|
||||||
*
|
|
||||||
* @param name The desired name for the Rule
|
|
||||||
*/
|
|
||||||
constructor(attribute, constraint, required, weight) {
|
|
||||||
// Create a new UUID for this Rule
|
|
||||||
this.id = guid();
|
|
||||||
|
|
||||||
// Set the Rule's weight (defaults to 1)
|
|
||||||
this.weight = weight || 1;
|
|
||||||
|
|
||||||
// If an attribute is specified, assign it
|
|
||||||
if (attribute) {
|
|
||||||
this.attribute = attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a constraint is specified, assign it
|
|
||||||
if (constraint) {
|
|
||||||
this.constraint = constraint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a value for 'required' is specified, assign it
|
|
||||||
if (required === undefined) {
|
|
||||||
// If no value was specified, default to False
|
|
||||||
this.required = false;
|
|
||||||
} else {
|
|
||||||
this.required = required;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache for constraint info object
|
|
||||||
this._constraintInfo = void 0;
|
|
||||||
|
|
||||||
// Cache for validator and value object
|
|
||||||
this._validatorAndValue = void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occasionally the Rule class needs to be instantiated from a JavaScript Object.
|
|
||||||
* This function fills in a Protocol with the Object data.
|
|
||||||
*
|
|
||||||
* @param input A Rule as a JavaScript Object, e.g. retrieved from JSON
|
|
||||||
*/
|
|
||||||
fromObject(input) {
|
|
||||||
// Check if the input already has an ID
|
|
||||||
// If so, keep it. It not, create a new UUID
|
|
||||||
this.id = input.id || guid();
|
|
||||||
|
|
||||||
// Assign the specified input data to the Rule
|
|
||||||
this.required = input.required;
|
|
||||||
this.weight = input.weight;
|
|
||||||
this.attribute = input.attribute;
|
|
||||||
this.constraint = input.constraint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the constraint info object for the current constraint
|
|
||||||
* @return {Object\undefined} Constraint object or undefined if current constraint
|
|
||||||
* is not valid or not found in comparators list
|
|
||||||
*/
|
|
||||||
getConstraintInfo() {
|
|
||||||
let constraintInfo = this._constraintInfo;
|
|
||||||
// Check if info is cached already
|
|
||||||
if (constraintInfo !== void 0) {
|
|
||||||
return constraintInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ruleConstraint = Object.keys(this.constraint)[0];
|
|
||||||
|
|
||||||
if (ruleConstraint !== void 0) {
|
|
||||||
constraintInfo = comparators.find(
|
|
||||||
comparator => ruleConstraint === comparator.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache this information for later use
|
|
||||||
this._constraintInfo = constraintInfo;
|
|
||||||
|
|
||||||
return constraintInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if current rule is related to priors
|
|
||||||
* @return {Boolean} True if a rule is related to priors or false otherwise
|
|
||||||
*/
|
|
||||||
isRuleForPrior() {
|
|
||||||
// @TODO: Should we check this too? this.attribute === 'relativeTime'
|
|
||||||
return this.attribute === 'abstractPriorValue';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the current rule is a rule for priors, returns the number of referenced priors. Otherwise, returns -1.
|
|
||||||
* @return {Number} The number of referenced priors or -1 if not applicable. Returns zero if the actual value could not be determined.
|
|
||||||
*/
|
|
||||||
getNumberOfPriorsReferenced() {
|
|
||||||
if (!this.isRuleForPrior()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get rule's validator and value
|
|
||||||
const ruleValidatorAndValue = this.getConstraintValidatorAndValue();
|
|
||||||
const { value, validator } = ruleValidatorAndValue;
|
|
||||||
const intValue = parseInt(value, 10) || 0; // avoid possible NaN
|
|
||||||
|
|
||||||
// "Equal to" validators
|
|
||||||
if (EQUALS_REGEXP.test(validator)) {
|
|
||||||
// In this case, -1 (the oldest prior) indicates that at least one study is used
|
|
||||||
return intValue < 0 ? 1 : intValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default cases return value
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the constraint validator and value
|
|
||||||
* @return {Object|undefined} Returns an object containing the validator and it's value or undefined
|
|
||||||
*/
|
|
||||||
getConstraintValidatorAndValue() {
|
|
||||||
let validatorAndValue = this._validatorAndValue;
|
|
||||||
|
|
||||||
// Check if validator and value are cached already
|
|
||||||
if (validatorAndValue !== void 0) {
|
|
||||||
return validatorAndValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the constraint info object
|
|
||||||
const constraintInfo = this.getConstraintInfo();
|
|
||||||
|
|
||||||
// Constraint info object exists and is valid
|
|
||||||
if (constraintInfo !== void 0) {
|
|
||||||
const validator = constraintInfo.validator;
|
|
||||||
const currentValidator = this.constraint[validator];
|
|
||||||
|
|
||||||
if (currentValidator) {
|
|
||||||
const constraintValidator = constraintInfo.validatorOption;
|
|
||||||
const constraintValue = currentValidator[constraintValidator];
|
|
||||||
|
|
||||||
validatorAndValue = {
|
|
||||||
value: constraintValue,
|
|
||||||
validator: constraintInfo.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._validatorAndValue = validatorAndValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return validatorAndValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import ViewportStructure from './ViewportStructure';
|
|
||||||
import Viewport from './Viewport';
|
|
||||||
import guid from '../../utils/guid';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Stage is one step in the Display Set Sequence for a Hanging Protocol
|
|
||||||
*
|
|
||||||
* Stages are defined as a ViewportStructure and an array of Viewports
|
|
||||||
*
|
|
||||||
* @type {Stage}
|
|
||||||
*/
|
|
||||||
export default class Stage {
|
|
||||||
constructor(ViewportStructure, name) {
|
|
||||||
// Create a new UUID for this Stage
|
|
||||||
this.id = guid();
|
|
||||||
|
|
||||||
// Assign the name and ViewportStructure provided
|
|
||||||
this.name = name;
|
|
||||||
this.viewportStructure = ViewportStructure;
|
|
||||||
|
|
||||||
// Create an empty array for the Viewports
|
|
||||||
this.viewports = [];
|
|
||||||
|
|
||||||
// Set the created date to Now
|
|
||||||
this.createdDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a clone of the current Stage with a new name
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @returns {Stage|*}
|
|
||||||
*/
|
|
||||||
createClone(name) {
|
|
||||||
// Create a new JavaScript independent of the current Protocol
|
|
||||||
var currentStage = Object.assign({}, this);
|
|
||||||
|
|
||||||
// Create a new Stage to return
|
|
||||||
var clonedStage = new Stage();
|
|
||||||
|
|
||||||
// Assign the desired properties
|
|
||||||
currentStage.id = clonedStage.id;
|
|
||||||
clonedStage.fromObject(currentStage);
|
|
||||||
|
|
||||||
// If we have specified a name, assign it
|
|
||||||
if (name) {
|
|
||||||
clonedStage.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the cloned Stage
|
|
||||||
return clonedStage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occasionally the Stage class needs to be instantiated from a JavaScript Object.
|
|
||||||
* This function fills in a Protocol with the Object data.
|
|
||||||
*
|
|
||||||
* @param input A Stage as a JavaScript Object, e.g. retrieved from JSON
|
|
||||||
*/
|
|
||||||
fromObject(input) {
|
|
||||||
// Check if the input already has an ID
|
|
||||||
// If so, keep it. It not, create a new UUID
|
|
||||||
this.id = input.id || guid();
|
|
||||||
|
|
||||||
// Assign the input name to the Stage
|
|
||||||
this.name = input.name;
|
|
||||||
|
|
||||||
// If a ViewportStructure is present in the input, add it from the
|
|
||||||
// input data
|
|
||||||
this.viewportStructure = new ViewportStructure();
|
|
||||||
this.viewportStructure.fromObject(input.viewportStructure);
|
|
||||||
|
|
||||||
// If any viewports are present in the input object
|
|
||||||
if (input.viewports) {
|
|
||||||
input.viewports.forEach(viewportObject => {
|
|
||||||
// Create a new Viewport with their data
|
|
||||||
var viewport = new Viewport();
|
|
||||||
viewport.fromObject(viewportObject);
|
|
||||||
|
|
||||||
// Add it to the viewports array
|
|
||||||
this.viewports.push(viewport);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import {
|
|
||||||
StudyMatchingRule,
|
|
||||||
SeriesMatchingRule,
|
|
||||||
ImageMatchingRule,
|
|
||||||
} from './rules';
|
|
||||||
import { removeFromArray } from '../lib/removeFromArray';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This Class defines a Viewport in the Hanging Protocol Stage. A Viewport contains
|
|
||||||
* arrays of Rules that are matched in the ProtocolEngine in order to determine which
|
|
||||||
* images should be hung.
|
|
||||||
*
|
|
||||||
* @type {Viewport}
|
|
||||||
*/
|
|
||||||
export default class Viewport {
|
|
||||||
constructor() {
|
|
||||||
this.viewportSettings = {};
|
|
||||||
this.imageMatchingRules = [];
|
|
||||||
this.seriesMatchingRules = [];
|
|
||||||
this.studyMatchingRules = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occasionally the Viewport class needs to be instantiated from a JavaScript Object.
|
|
||||||
* This function fills in a Viewport with the Object data.
|
|
||||||
*
|
|
||||||
* @param input The Viewport as a JavaScript Object, e.g. retrieved from JSON
|
|
||||||
*/
|
|
||||||
fromObject(input) {
|
|
||||||
// If ImageMatchingRules exist, create them from the Object data
|
|
||||||
// and add them to the Viewport's imageMatchingRules array
|
|
||||||
if (input.imageMatchingRules) {
|
|
||||||
input.imageMatchingRules.forEach(ruleObject => {
|
|
||||||
var rule = new ImageMatchingRule();
|
|
||||||
rule.fromObject(ruleObject);
|
|
||||||
this.imageMatchingRules.push(rule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If SeriesMatchingRules exist, create them from the Object data
|
|
||||||
// and add them to the Viewport's seriesMatchingRules array
|
|
||||||
if (input.seriesMatchingRules) {
|
|
||||||
input.seriesMatchingRules.forEach(ruleObject => {
|
|
||||||
var rule = new SeriesMatchingRule();
|
|
||||||
rule.fromObject(ruleObject);
|
|
||||||
this.seriesMatchingRules.push(rule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If StudyMatchingRules exist, create them from the Object data
|
|
||||||
// and add them to the Viewport's studyMatchingRules array
|
|
||||||
if (input.studyMatchingRules) {
|
|
||||||
input.studyMatchingRules.forEach(ruleObject => {
|
|
||||||
var rule = new StudyMatchingRule();
|
|
||||||
rule.fromObject(ruleObject);
|
|
||||||
this.studyMatchingRules.push(rule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If ViewportSettings exist, add them to the current protocol
|
|
||||||
if (input.viewportSettings) {
|
|
||||||
this.viewportSettings = input.viewportSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds and removes a rule from whichever array it exists in.
|
|
||||||
* It is not required to specify if it exists in studyMatchingRules,
|
|
||||||
* seriesMatchingRules, or imageMatchingRules
|
|
||||||
*
|
|
||||||
* @param rule
|
|
||||||
*/
|
|
||||||
removeRule(rule) {
|
|
||||||
var array;
|
|
||||||
if (rule instanceof StudyMatchingRule) {
|
|
||||||
array = this.studyMatchingRules;
|
|
||||||
} else if (rule instanceof SeriesMatchingRule) {
|
|
||||||
array = this.seriesMatchingRules;
|
|
||||||
} else if (rule instanceof ImageMatchingRule) {
|
|
||||||
array = this.imageMatchingRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromArray(array, rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* The ViewportStructure class represents the layout and layout properties that
|
|
||||||
* Viewports are displayed in. ViewportStructure has a type, which corresponds to
|
|
||||||
* a layout template, and a set of properties, which depend on the type.
|
|
||||||
*
|
|
||||||
* @type {ViewportStructure}
|
|
||||||
*/
|
|
||||||
export default class ViewportStructure {
|
|
||||||
constructor(type, properties) {
|
|
||||||
this.type = type;
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occasionally the ViewportStructure class needs to be instantiated from a JavaScript Object.
|
|
||||||
* This function fills in a ViewportStructure with the Object data.
|
|
||||||
*
|
|
||||||
* @param input The ViewportStructure as a JavaScript Object, e.g. retrieved from JSON
|
|
||||||
*/
|
|
||||||
fromObject(input) {
|
|
||||||
this.type = input.type;
|
|
||||||
this.properties = input.properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the layout template name based on the layout type
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getLayoutTemplateName() {
|
|
||||||
// Viewport structure can be updated later when we build more complex display layouts
|
|
||||||
switch (this.type) {
|
|
||||||
case 'grid':
|
|
||||||
return 'gridLayout';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of Viewports required for this layout
|
|
||||||
* given the layout type and properties
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getNumViewports() {
|
|
||||||
// Viewport structure can be updated later when we build more complex display layouts
|
|
||||||
switch (this.type) {
|
|
||||||
case 'grid':
|
|
||||||
// For the typical grid layout, we only need to multiply Rows by Columns to
|
|
||||||
// obtain the number of viewports
|
|
||||||
return this.properties.Rows * this.properties.Columns;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import Protocol from './Protocol.js';
|
|
||||||
import Rule from './Rule.js';
|
|
||||||
import Stage from './Stage.js';
|
|
||||||
import Viewport from './Viewport.js';
|
|
||||||
import ViewportStructure from './ViewportStructure.js';
|
|
||||||
import {
|
|
||||||
ProtocolMatchingRule,
|
|
||||||
StudyMatchingRule,
|
|
||||||
SeriesMatchingRule,
|
|
||||||
ImageMatchingRule,
|
|
||||||
} from './rules.js';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Protocol,
|
|
||||||
Rule,
|
|
||||||
Stage,
|
|
||||||
Viewport,
|
|
||||||
ViewportStructure,
|
|
||||||
ProtocolMatchingRule,
|
|
||||||
StudyMatchingRule,
|
|
||||||
SeriesMatchingRule,
|
|
||||||
ImageMatchingRule,
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import Rule from './Rule';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ProtocolMatchingRule Class extends the Rule Class.
|
|
||||||
*
|
|
||||||
* At present it does not add any new methods or attributes
|
|
||||||
* @type {ProtocolMatchingRule}
|
|
||||||
*/
|
|
||||||
class ProtocolMatchingRule extends Rule {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The StudyMatchingRule Class extends the Rule Class.
|
|
||||||
*
|
|
||||||
* At present it does not add any new methods or attributes
|
|
||||||
* @type {StudyMatchingRule}
|
|
||||||
*/
|
|
||||||
class StudyMatchingRule extends Rule {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SeriesMatchingRule Class extends the Rule Class.
|
|
||||||
*
|
|
||||||
* At present it does not add any new methods or attributes
|
|
||||||
* @type {SeriesMatchingRule}
|
|
||||||
*/
|
|
||||||
class SeriesMatchingRule extends Rule {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ImageMatchingRule class extends the Rule Class.
|
|
||||||
*
|
|
||||||
* At present it does not add any new methods or attributes
|
|
||||||
* @type {ImageMatchingRule}
|
|
||||||
*/
|
|
||||||
class ImageMatchingRule extends Rule {}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ProtocolMatchingRule,
|
|
||||||
StudyMatchingRule,
|
|
||||||
SeriesMatchingRule,
|
|
||||||
ImageMatchingRule,
|
|
||||||
};
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
// Define an empty object to store callbacks that are used to retrieve custom attributes
|
|
||||||
// The simplest example for a custom attribute is the Timepoint type (i.e. baseline or follow-up)
|
|
||||||
// used in the LesionTracker application.
|
|
||||||
//
|
|
||||||
// Timepoint type can be obtained given a studyId, and this is done through a custom callback.
|
|
||||||
// Developers can define attributes (i.e. attributeId = timepointType) with a name ('Timepoint Type')
|
|
||||||
// and a callback function that is used to calculate them.
|
|
||||||
//
|
|
||||||
// The input to the callback, which is called during viewport-image matching rule evaluation
|
|
||||||
// is the set of attributes that contains the specified attribute. In our example, timepointType is
|
|
||||||
// linked to the study attributes, and so the inputs to the callback is an object containing
|
|
||||||
// the study attributes.
|
|
||||||
const CustomAttributeRetrievalCallbacks = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
|
|
||||||
* callback that will be used to calculate the attribute value.
|
|
||||||
*
|
|
||||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
|
||||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
|
||||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
|
||||||
*/
|
|
||||||
function addCustomAttribute(attributeId, attributeName, callback) {
|
|
||||||
CustomAttributeRetrievalCallbacks[attributeId] = {
|
|
||||||
name: attributeName,
|
|
||||||
callback: callback,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { CustomAttributeRetrievalCallbacks, addCustomAttribute };
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
// Define an empty object to store callbacks that are used to apply custom viewport settings
|
|
||||||
// after a viewport is rendered.
|
|
||||||
const CustomViewportSettings = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
|
|
||||||
*
|
|
||||||
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
|
|
||||||
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
|
|
||||||
* @param options
|
|
||||||
* @param callback A function to be run after a viewport is rendered with a series
|
|
||||||
*/
|
|
||||||
function addCustomViewportSetting(settingId, settingName, options, callback) {
|
|
||||||
CustomViewportSettings[settingId] = {
|
|
||||||
id: settingId,
|
|
||||||
text: settingName,
|
|
||||||
options: options,
|
|
||||||
callback: callback,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { CustomViewportSettings, addCustomViewportSetting };
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
export const attributeDefaults = {
|
|
||||||
abstractPriorValue: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const displaySettings = {
|
|
||||||
invert: {
|
|
||||||
id: 'invert',
|
|
||||||
text: 'Show Grayscale Inverted',
|
|
||||||
defaultValue: 'NO',
|
|
||||||
options: ['YES', 'NO'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO Fix abstractPriorValue comparison
|
|
||||||
export const studyAttributes = [
|
|
||||||
{
|
|
||||||
id: 'x00100020',
|
|
||||||
text: '(x00100020) Patient ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x0020000d',
|
|
||||||
text: '(x0020000d) Study Instance UID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080020',
|
|
||||||
text: '(x00080020) Study Date',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080030',
|
|
||||||
text: '(x00080030) Study Time',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00081030',
|
|
||||||
text: '(x00081030) Study Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'abstractPriorValue',
|
|
||||||
text: 'Abstract Prior Value',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const protocolAttributes = [
|
|
||||||
{
|
|
||||||
id: 'x00100020',
|
|
||||||
text: '(x00100020) Patient ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x0020000d',
|
|
||||||
text: '(x0020000d) Study Instance UID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080020',
|
|
||||||
text: '(x00080020) Study Date',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080030',
|
|
||||||
text: '(x00080030) Study Time',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00081030',
|
|
||||||
text: '(x00081030) Study Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'anatomicRegion',
|
|
||||||
text: 'Anatomic Region',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const seriesAttributes = [
|
|
||||||
{
|
|
||||||
id: 'x0020000e',
|
|
||||||
text: '(x0020000e) Series Instance UID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080060',
|
|
||||||
text: '(x00080060) Modality',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00200011',
|
|
||||||
text: '(x00200011) Series Number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x0008103e',
|
|
||||||
text: '(x0008103e) Series Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'numImages',
|
|
||||||
text: 'Number of Images',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const instanceAttributes = [
|
|
||||||
{
|
|
||||||
id: 'x00080016',
|
|
||||||
text: '(x00080016) SOP Class UID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080018',
|
|
||||||
text: '(x00080018) SOP Instance UID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00185101',
|
|
||||||
text: '(x00185101) View Position',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00200013',
|
|
||||||
text: '(x00200013) Instance Number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00080008',
|
|
||||||
text: '(x00080008) Image Type',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00181063',
|
|
||||||
text: '(x00181063) Frame Time',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00200060',
|
|
||||||
text: '(x00200060) Laterality',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00541330',
|
|
||||||
text: '(x00541330) Image Index',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00280004',
|
|
||||||
text: '(x00280004) Photometric Interpretation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x00180050',
|
|
||||||
text: '(x00180050) Slice Thickness',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import ProtocolEngine from './ProtocolEngine.js';
|
|
||||||
import { ProtocolStore, ProtocolStrategy } from './protocolStore';
|
|
||||||
import { addCustomAttribute } from './customAttributes';
|
|
||||||
import { addCustomViewportSetting } from './customViewportSettings';
|
|
||||||
|
|
||||||
const hangingProtocols = {
|
|
||||||
ProtocolEngine,
|
|
||||||
ProtocolStore,
|
|
||||||
ProtocolStrategy,
|
|
||||||
addCustomAttribute,
|
|
||||||
addCustomViewportSetting,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default hangingProtocols;
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
const comparators = [
|
|
||||||
{
|
|
||||||
id: 'equals',
|
|
||||||
name: '= (Equals)',
|
|
||||||
validator: 'equals',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must equal this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'doesNotEqual',
|
|
||||||
name: '!= (Does not equal)',
|
|
||||||
validator: 'doesNotEqual',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must not equal this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'contains',
|
|
||||||
name: 'Contains',
|
|
||||||
validator: 'contains',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must contain this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'doesNotContain',
|
|
||||||
name: 'Does not contain',
|
|
||||||
validator: 'doesNotContain',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must not contain this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'startsWith',
|
|
||||||
name: 'Starts with',
|
|
||||||
validator: 'startsWith',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must start with this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'endsWith',
|
|
||||||
name: 'Ends with',
|
|
||||||
validator: 'endsWith',
|
|
||||||
validatorOption: 'value',
|
|
||||||
description: 'The attribute must end with this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'onlyInteger',
|
|
||||||
name: 'Only Integers',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'onlyInteger',
|
|
||||||
description: "Real numbers won't be allowed.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'greaterThan',
|
|
||||||
name: '> (Greater than)',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'greaterThan',
|
|
||||||
description: 'The attribute has to be greater than this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'greaterThanOrEqualTo',
|
|
||||||
name: '>= (Greater than or equal to)',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'greaterThanOrEqualTo',
|
|
||||||
description: 'The attribute has to be at least this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lessThanOrEqualTo',
|
|
||||||
name: '<= (Less than or equal to)',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'lessThanOrEqualTo',
|
|
||||||
description: 'The attribute can be this value at the most.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lessThan',
|
|
||||||
name: '< (Less than)',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'lessThan',
|
|
||||||
description: 'The attribute has to be less than this value.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'odd',
|
|
||||||
name: 'Odd',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'odd',
|
|
||||||
description: 'The attribute has to be odd.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'even',
|
|
||||||
name: 'Even',
|
|
||||||
validator: 'numericality',
|
|
||||||
validatorOption: 'even',
|
|
||||||
description: 'The attribute has to be even.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Immutable object
|
|
||||||
Object.freeze(comparators);
|
|
||||||
|
|
||||||
export { comparators };
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
const attributeCache = Object.create(null);
|
|
||||||
const REGEXP = /^\([x0-9a-f]+\)/;
|
|
||||||
|
|
||||||
const humanize = text => {
|
|
||||||
let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
|
|
||||||
|
|
||||||
humanized = humanized.replace(/^./, str => {
|
|
||||||
// uppercase the first character
|
|
||||||
return str.toUpperCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
return humanized;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the text of an attribute for a given attribute
|
|
||||||
* @param {String} attributeId The attribute ID
|
|
||||||
* @param {Array} attributes Array of attributes objects with id and text properties
|
|
||||||
* @return {String} If found return the attribute text or an empty string otherwise
|
|
||||||
*/
|
|
||||||
const getAttributeText = (attributeId, attributes) => {
|
|
||||||
// If the attribute is already in the cache, return it
|
|
||||||
if (attributeId in attributeCache) {
|
|
||||||
return attributeCache[attributeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the attribute with given attributeId
|
|
||||||
const attribute = attributes.find(attribute => attribute.id === attributeId);
|
|
||||||
|
|
||||||
let attributeText;
|
|
||||||
|
|
||||||
// If attribute was found get its text and save it on the cache
|
|
||||||
if (attribute) {
|
|
||||||
attributeText = attribute.text.replace(REGEXP, '');
|
|
||||||
attributeCache[attributeId] = attributeText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributeText || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
function displayConstraint(attributeId, constraint, attributes) {
|
|
||||||
if (!constraint || !attributeId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatorType = Object.keys(constraint)[0];
|
|
||||||
if (!validatorType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validator = Object.keys(constraint[validatorType])[0];
|
|
||||||
if (!validator) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = constraint[validatorType][validator];
|
|
||||||
if (value === void 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let comparator = validator;
|
|
||||||
if (validator === 'value') {
|
|
||||||
comparator = validatorType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributeText = getAttributeText(attributeId, attributes);
|
|
||||||
const constraintText =
|
|
||||||
attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
|
|
||||||
|
|
||||||
return constraintText;
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Removes the first instance of an element from an array, if an equal value exists
|
|
||||||
*
|
|
||||||
* @param array
|
|
||||||
* @param input
|
|
||||||
*
|
|
||||||
* @returns {boolean} Whether or not the element was found and removed
|
|
||||||
*/
|
|
||||||
const removeFromArray = (array, input) => {
|
|
||||||
// If the array is empty, stop here
|
|
||||||
if (!array || !array.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
array.forEach((value, index) => {
|
|
||||||
// TODO: Double check whether or not this deep equality check is necessary
|
|
||||||
//if (_.isEqual(value, input)) {
|
|
||||||
if (value === input) {
|
|
||||||
indexToRemove = index;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (indexToRemove === void 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
array.splice(indexToRemove, 1);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { removeFromArray };
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
// Sorts an array by score
|
|
||||||
const sortByScore = arr => {
|
|
||||||
arr.sort((a, b) => {
|
|
||||||
return b.score - a.score;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { sortByScore };
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import validate from 'validate.js';
|
|
||||||
|
|
||||||
validate.validators.equals = function(value, options, key, attributes) {
|
|
||||||
if (options && value !== options.value) {
|
|
||||||
return key + 'must equal ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validate.validators.doesNotEqual = function(value, options, key) {
|
|
||||||
if (options && value === options.value) {
|
|
||||||
return key + 'cannot equal ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validate.validators.contains = function(value, options, key) {
|
|
||||||
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
|
||||||
return key + 'must contain ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validate.validators.doesNotContain = function(value, options, key) {
|
|
||||||
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
|
||||||
return key + 'cannot contain ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validate.validators.startsWith = function(value, options, key) {
|
|
||||||
if (options && value.startsWith && !value.startsWith(options.value)) {
|
|
||||||
return key + 'must start with ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validate.validators.endsWith = function(value, options, key) {
|
|
||||||
if (options && value.endsWith && !value.endsWith(options.value)) {
|
|
||||||
return key + 'must end with ' + options.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { validate };
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import Protocol from '../../classes/Protocol';
|
|
||||||
|
|
||||||
// The ProtocolStore class allows persisting hanging protocols using different strategies.
|
|
||||||
// For example, one strategy stores hanging protocols in the application server while
|
|
||||||
// another strategy stores them in a remote machine, but only one strategy can be used at a time.
|
|
||||||
|
|
||||||
export default class ProtocolStore {
|
|
||||||
constructor(strategy) {
|
|
||||||
this.strategy = strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a Protocol instance or array of Protocol instances for the given protocol object or array
|
|
||||||
* @param {Object|array} protocolObject Protocol plain object or array of Protocol plain objects
|
|
||||||
* @return {Protocol|array} Protocol instance or array of Protocol intances for the given protocol object or array
|
|
||||||
*/
|
|
||||||
static getProtocolInstance(protocolObject) {
|
|
||||||
let result = protocolObject;
|
|
||||||
|
|
||||||
// If result is an array of protocols objects
|
|
||||||
if (result instanceof Array) {
|
|
||||||
result.forEach((protocol, index) => {
|
|
||||||
// Check if protocol is an instance of Protocol
|
|
||||||
if (!(protocol instanceof Protocol)) {
|
|
||||||
const protocolInstance = new Protocol();
|
|
||||||
protocolInstance.fromObject(protocol);
|
|
||||||
result[index] = protocolInstance;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (result !== void 0 && !(result instanceof Protocol)) {
|
|
||||||
// Check if result exists and is not an instance of Protocol
|
|
||||||
const protocolInstance = new Protocol();
|
|
||||||
protocolInstance.fromObject(result);
|
|
||||||
result = protocolInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a function to be called when the protocol store is ready to persist hanging protocols
|
|
||||||
*
|
|
||||||
* NOTE: Strategies should implement this function
|
|
||||||
*
|
|
||||||
* @param callback The function to be called as a callback
|
|
||||||
*/
|
|
||||||
onReady(callback) {
|
|
||||||
this.strategy.onReady(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
|
|
||||||
*
|
|
||||||
* NOTE: Strategies should implement this function
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to find the hanging protocol
|
|
||||||
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
|
|
||||||
*/
|
|
||||||
getProtocol(protocolId) {
|
|
||||||
let result = this.strategy.getProtocol(protocolId);
|
|
||||||
return ProtocolStore.getProtocolInstance(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the hanging protocol
|
|
||||||
*
|
|
||||||
* NOTE: Strategies should implement this function
|
|
||||||
*
|
|
||||||
* @param protocol The hanging protocol to be stored
|
|
||||||
*/
|
|
||||||
addProtocol(protocol) {
|
|
||||||
this.strategy.addProtocol(protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the hanging protocol by protocolId
|
|
||||||
*
|
|
||||||
* NOTE: Strategies should implement this function
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to find the hanging protocol to update
|
|
||||||
* @param protocol The updated hanging protocol
|
|
||||||
*/
|
|
||||||
updateProtocol(protocolId, protocol) {
|
|
||||||
this.strategy.updateProtocol(protocolId, protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the hanging protocol
|
|
||||||
*
|
|
||||||
* NOTE: Strategies should implement this function
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to remove the hanging protocol
|
|
||||||
*/
|
|
||||||
removeProtocol(protocolId) {
|
|
||||||
this.strategy.removeProtocol(protocolId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import log from '../../../log';
|
|
||||||
import defaultProtocol from '../defaultProtocol';
|
|
||||||
|
|
||||||
export default class ProtocolStrategy {
|
|
||||||
constructor() {
|
|
||||||
this.hangingProtocols = new Map();
|
|
||||||
this.defaultsAdded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a function to be called when the hangingProtocols collection is subscribed
|
|
||||||
* The callback is called only one time when the subscription is ready
|
|
||||||
*
|
|
||||||
* @param callback The function to be called as a callback
|
|
||||||
*/
|
|
||||||
onReady(callback) {
|
|
||||||
if (!this.defaultsAdded) {
|
|
||||||
log.info('Inserting the default hanging protocol...');
|
|
||||||
this.addProtocol(defaultProtocol);
|
|
||||||
this.defaultsAdded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to find the hanging protocol
|
|
||||||
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
|
|
||||||
*/
|
|
||||||
getProtocol(protocolId) {
|
|
||||||
// Return the hanging protocol by protocolId if defined
|
|
||||||
if (protocolId) {
|
|
||||||
return this.hangingProtocols.get(protocolId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return all protocols
|
|
||||||
return Array.from(this.hangingProtocols.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the hanging protocol
|
|
||||||
*
|
|
||||||
* @param protocol The hanging protocol to be stored
|
|
||||||
*/
|
|
||||||
addProtocol(protocol) {
|
|
||||||
this.hangingProtocols.set(protocol.id, protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the hanging protocol by protocolId
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to find the hanging protocol to update
|
|
||||||
* @param protocol The updated hanging protocol
|
|
||||||
*/
|
|
||||||
updateProtocol(protocolId, protocol) {
|
|
||||||
if (!this.hangingProtocols.has(protocolId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hangingProtocols.set(protocolId, protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the hanging protocol
|
|
||||||
*
|
|
||||||
* @param protocolId The protocol ID used to remove the hanging protocol
|
|
||||||
*/
|
|
||||||
removeProtocol(protocolId) {
|
|
||||||
if (!this.hangingProtocols.has(protocolId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hangingProtocols.delete(protocolId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import ProtocolStore from './ProtocolStore';
|
|
||||||
import ProtocolStrategy from './ProtocolStrategy';
|
|
||||||
|
|
||||||
export { ProtocolStore, ProtocolStrategy };
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import Protocol from '../classes/Protocol';
|
|
||||||
import ViewportStructure from '../classes/ViewportStructure';
|
|
||||||
import Viewport from '../classes/Viewport';
|
|
||||||
import Stage from '../classes/Stage';
|
|
||||||
|
|
||||||
function getDefaultProtocol() {
|
|
||||||
const protocol = new Protocol('Default');
|
|
||||||
protocol.id = 'defaultProtocol';
|
|
||||||
protocol.locked = true;
|
|
||||||
|
|
||||||
const oneByOne = new ViewportStructure('grid', {
|
|
||||||
Rows: 1,
|
|
||||||
Columns: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const viewport = new Viewport();
|
|
||||||
const first = new Stage(oneByOne, 'oneByOne');
|
|
||||||
first.viewports.push(viewport);
|
|
||||||
|
|
||||||
protocol.stages.push(first);
|
|
||||||
|
|
||||||
return protocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProtocol = getDefaultProtocol();
|
|
||||||
|
|
||||||
export default defaultProtocol;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { ProtocolStore, ProtocolStrategy } from './classes';
|
|
||||||
import defaultProtocol from './defaultProtocol';
|
|
||||||
import testProtocols from './testProtocols';
|
|
||||||
|
|
||||||
export { ProtocolStore, ProtocolStrategy, defaultProtocol, testProtocols };
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@
|
|||||||
//import Dropdown from './ui/dropdown/class.js';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Defines the base OHIF header object
|
|
||||||
*/
|
|
||||||
//const dropdown = new OHIF.ui.Dropdown();
|
|
||||||
const header = {};
|
|
||||||
|
|
||||||
export default header;
|
|
||||||
@ -1,23 +1,13 @@
|
|||||||
import './lib';
|
|
||||||
|
|
||||||
import { ExtensionManager, MODULE_TYPES } from './extensions';
|
import { ExtensionManager, MODULE_TYPES } from './extensions';
|
||||||
import { ServicesManager } from './services';
|
import { ServicesManager } from './services';
|
||||||
import classes, { CommandsManager, HotkeysManager } from './classes/';
|
import classes, { CommandsManager, HotkeysManager } from './classes/';
|
||||||
|
|
||||||
import DICOMWeb from './DICOMWeb';
|
import DICOMWeb from './DICOMWeb';
|
||||||
import DICOMSR from './DICOMSR';
|
import DICOMSR from './DICOMSR';
|
||||||
import cornerstone from './cornerstone.js';
|
|
||||||
import errorHandler from './errorHandler.js';
|
import errorHandler from './errorHandler.js';
|
||||||
import hangingProtocols from './hanging-protocols';
|
|
||||||
import header from './header.js';
|
|
||||||
import log from './log.js';
|
import log from './log.js';
|
||||||
import measurements from './measurements';
|
|
||||||
import metadata from './classes/metadata/';
|
|
||||||
import object from './object.js';
|
import object from './object.js';
|
||||||
import redux from './redux/';
|
|
||||||
import string from './string.js';
|
import string from './string.js';
|
||||||
import studies from './studies/';
|
|
||||||
import ui from './ui';
|
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import { ViewModelProvider, useViewModel } from './ViewModelContext';
|
import { ViewModelProvider, useViewModel } from './ViewModelContext';
|
||||||
import utils from './utils/';
|
import utils from './utils/';
|
||||||
@ -42,7 +32,7 @@ import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
|||||||
|
|
||||||
const hotkeys = {
|
const hotkeys = {
|
||||||
...utils.hotkeys,
|
...utils.hotkeys,
|
||||||
defaults: { hotkeyBindings: defaults.hotkeyBindings }
|
defaults: { hotkeyBindings: defaults.hotkeyBindings },
|
||||||
};
|
};
|
||||||
|
|
||||||
const OHIF = {
|
const OHIF = {
|
||||||
@ -56,14 +46,8 @@ const OHIF = {
|
|||||||
defaults,
|
defaults,
|
||||||
utils,
|
utils,
|
||||||
hotkeys,
|
hotkeys,
|
||||||
studies,
|
|
||||||
redux,
|
|
||||||
classes,
|
classes,
|
||||||
metadata,
|
|
||||||
header,
|
|
||||||
cornerstone,
|
|
||||||
string,
|
string,
|
||||||
ui,
|
|
||||||
user,
|
user,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
object,
|
object,
|
||||||
@ -71,8 +55,6 @@ const OHIF = {
|
|||||||
DICOMWeb,
|
DICOMWeb,
|
||||||
DICOMSR,
|
DICOMSR,
|
||||||
viewer: {},
|
viewer: {},
|
||||||
measurements,
|
|
||||||
hangingProtocols,
|
|
||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
@ -102,22 +84,14 @@ export {
|
|||||||
defaults,
|
defaults,
|
||||||
utils,
|
utils,
|
||||||
hotkeys,
|
hotkeys,
|
||||||
studies,
|
|
||||||
redux,
|
|
||||||
classes,
|
classes,
|
||||||
metadata,
|
|
||||||
header,
|
|
||||||
cornerstone,
|
|
||||||
string,
|
string,
|
||||||
ui,
|
|
||||||
user,
|
user,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
object,
|
object,
|
||||||
log,
|
log,
|
||||||
DICOMWeb,
|
DICOMWeb,
|
||||||
DICOMSR,
|
DICOMSR,
|
||||||
measurements,
|
|
||||||
hangingProtocols,
|
|
||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
|||||||
@ -1,327 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
function getBoundingBox(context, textLines, x, y, options) {
|
|
||||||
if (Object.prototype.toString.call(textLines) !== '[object Array]') {
|
|
||||||
textLines = [textLines];
|
|
||||||
}
|
|
||||||
|
|
||||||
const padding = 5;
|
|
||||||
const font = cornerstoneTools.textStyle.getFont();
|
|
||||||
const fontSize = cornerstoneTools.textStyle.getFontSize();
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
context.font = font;
|
|
||||||
context.textBaseline = 'top';
|
|
||||||
|
|
||||||
// Find the longest text width in the array of text data
|
|
||||||
let maxWidth = 0;
|
|
||||||
|
|
||||||
textLines.forEach(text => {
|
|
||||||
// Get the text width in the current font
|
|
||||||
const width = context.measureText(text).width;
|
|
||||||
|
|
||||||
// Find the maximum with for all the text Rows;
|
|
||||||
maxWidth = Math.max(maxWidth, width);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate the bounding box for this text box
|
|
||||||
const boundingBox = {
|
|
||||||
width: maxWidth + padding * 2,
|
|
||||||
height: padding + textLines.length * (fontSize + padding),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options && options.centering && options.centering.x === true) {
|
|
||||||
x -= boundingBox.width / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options && options.centering && options.centering.y === true) {
|
|
||||||
y -= boundingBox.height / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
boundingBox.left = x;
|
|
||||||
boundingBox.top = y;
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
|
|
||||||
// Return the bounding box so it can be used for pointNearHandle
|
|
||||||
return boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pixelToPage(element, position) {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
const result = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop here if the cornerstone element is not enabled or position is not an object
|
|
||||||
if (!enabledElement || typeof position !== 'object') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = enabledElement.canvas;
|
|
||||||
|
|
||||||
const canvasOffset = $(canvas).offset();
|
|
||||||
result.x += canvasOffset.left;
|
|
||||||
result.y += canvasOffset.top;
|
|
||||||
|
|
||||||
const canvasPosition = cornerstone.pixelToCanvas(element, position);
|
|
||||||
result.x += canvasPosition.x;
|
|
||||||
result.y += canvasPosition.y;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function repositionTextBox(eventData, measurementData, config) {
|
|
||||||
// Stop here if it's not a measurement creating
|
|
||||||
if (!measurementData.isCreating) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = eventData.element;
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
const image = enabledElement.image;
|
|
||||||
|
|
||||||
const allowedBorders = OHIF.uiSettings.autoPositionMeasurementsTextCallOuts;
|
|
||||||
const allow = {
|
|
||||||
T: !allowedBorders || allowedBorders.includes('T'),
|
|
||||||
R: !allowedBorders || allowedBorders.includes('R'),
|
|
||||||
B: !allowedBorders || allowedBorders.includes('B'),
|
|
||||||
L: !allowedBorders || allowedBorders.includes('L'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailableBlankAreas = (enabledElement, labelWidth, labelHeight) => {
|
|
||||||
const { element, canvas, image } = enabledElement;
|
|
||||||
|
|
||||||
const topLeft = cornerstone.pixelToCanvas(element, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bottomRight = cornerstone.pixelToCanvas(element, {
|
|
||||||
x: image.width,
|
|
||||||
y: image.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
const $canvas = $(canvas);
|
|
||||||
const canvasWidth = $canvas.outerWidth();
|
|
||||||
const canvasHeight = $canvas.outerHeight();
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
result['x-1'] = allow.L && topLeft.x > labelWidth;
|
|
||||||
result['y-1'] = allow.T && topLeft.y > labelHeight;
|
|
||||||
result.x1 = allow.R && canvasWidth - bottomRight.x > labelWidth;
|
|
||||||
result.y1 = allow.B && canvasHeight - bottomRight.y > labelHeight;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRenderingInformation = (limits, tool) => {
|
|
||||||
const mid = {};
|
|
||||||
mid.x = limits.x / 2;
|
|
||||||
mid.y = limits.y / 2;
|
|
||||||
|
|
||||||
const directions = {};
|
|
||||||
directions.x = tool.x < mid.x ? -1 : 1;
|
|
||||||
directions.y = tool.y < mid.y ? -1 : 1;
|
|
||||||
|
|
||||||
const diffX = directions.x < 0 ? tool.x : limits.x - tool.x;
|
|
||||||
const diffY = directions.y < 0 ? tool.y : limits.y - tool.y;
|
|
||||||
let cornerAxis = diffY < diffX ? 'y' : 'x';
|
|
||||||
|
|
||||||
const map = {
|
|
||||||
'x-1': 'L',
|
|
||||||
'y-1': 'T',
|
|
||||||
x1: 'R',
|
|
||||||
y1: 'B',
|
|
||||||
};
|
|
||||||
|
|
||||||
let current = 0;
|
|
||||||
while (current < 4 && !allow[map[cornerAxis + directions[cornerAxis]]]) {
|
|
||||||
// Invert the direction for the next iteration
|
|
||||||
directions[cornerAxis] *= -1;
|
|
||||||
|
|
||||||
// Invert the tempCornerAxis
|
|
||||||
cornerAxis = cornerAxis === 'x' ? 'y' : 'x';
|
|
||||||
|
|
||||||
current++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
directions,
|
|
||||||
cornerAxis,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateAxisCenter = (axis, start, end) => {
|
|
||||||
const a = start[axis];
|
|
||||||
const b = end[axis];
|
|
||||||
const lowest = Math.min(a, b);
|
|
||||||
const highest = Math.max(a, b);
|
|
||||||
return lowest + (highest - lowest) / 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTextBoxSizeInPixels = (element, bounds) => {
|
|
||||||
const topLeft = cornerstone.pageToPixel(element, 0, 0);
|
|
||||||
const bottomRight = cornerstone.pageToPixel(element, bounds.x, bounds.y);
|
|
||||||
return {
|
|
||||||
x: bottomRight.x - topLeft.x,
|
|
||||||
y: bottomRight.y - topLeft.y,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTextBoxOffset(config, cornerAxis, toolAxis, boxSize) {
|
|
||||||
config = config || {};
|
|
||||||
const centering = config.centering || {};
|
|
||||||
const centerX = !!centering.x;
|
|
||||||
const centerY = !!centering.y;
|
|
||||||
const halfBoxSizeX = boxSize.x / 2;
|
|
||||||
const halfBoxSizeY = boxSize.y / 2;
|
|
||||||
const offset = {
|
|
||||||
x: [],
|
|
||||||
y: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cornerAxis === 'x') {
|
|
||||||
const offsetY = centerY ? 0 : halfBoxSizeY;
|
|
||||||
|
|
||||||
offset.x[-1] = centerX ? halfBoxSizeX : 0;
|
|
||||||
offset.x[1] = centerX ? -halfBoxSizeX : -boxSize.x;
|
|
||||||
offset.y[-1] = offsetY;
|
|
||||||
offset.y[1] = offsetY;
|
|
||||||
} else {
|
|
||||||
const offsetX = centerX ? 0 : halfBoxSizeX;
|
|
||||||
|
|
||||||
offset.x[-1] = offsetX;
|
|
||||||
offset.x[1] = offsetX;
|
|
||||||
offset.y[-1] = centerY ? halfBoxSizeY : 0;
|
|
||||||
offset.y[1] = centerY ? -halfBoxSizeY : -boxSize.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handles = measurementData.handles;
|
|
||||||
const textBox = handles.textBox;
|
|
||||||
|
|
||||||
const $canvas = $(enabledElement.canvas);
|
|
||||||
const canvasWidth = $canvas.outerWidth();
|
|
||||||
const canvasHeight = $canvas.outerHeight();
|
|
||||||
const offset = $canvas.offset();
|
|
||||||
const canvasDimensions = {
|
|
||||||
x: canvasWidth,
|
|
||||||
y: canvasHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bounds = {};
|
|
||||||
bounds.x = textBox.boundingBox.width;
|
|
||||||
bounds.y = textBox.boundingBox.height;
|
|
||||||
|
|
||||||
const getHandlePosition = key => {
|
|
||||||
const { x, y } = handles[key];
|
|
||||||
|
|
||||||
return { x, y };
|
|
||||||
};
|
|
||||||
const start = getHandlePosition('start');
|
|
||||||
const end = getHandlePosition('end');
|
|
||||||
|
|
||||||
const tool = {};
|
|
||||||
tool.x = calculateAxisCenter('x', start, end);
|
|
||||||
tool.y = calculateAxisCenter('y', start, end);
|
|
||||||
|
|
||||||
let limits = {};
|
|
||||||
limits.x = image.width;
|
|
||||||
limits.y = image.height;
|
|
||||||
|
|
||||||
let { directions, cornerAxis } = getRenderingInformation(limits, tool);
|
|
||||||
|
|
||||||
const availableAreas = getAvailableBlankAreas(
|
|
||||||
enabledElement,
|
|
||||||
bounds.x,
|
|
||||||
bounds.y
|
|
||||||
);
|
|
||||||
const tempDirections = Object.assign({}, directions);
|
|
||||||
let tempCornerAxis = cornerAxis;
|
|
||||||
let foundPlace = false;
|
|
||||||
let current = 0;
|
|
||||||
while (current < 4) {
|
|
||||||
if (availableAreas[tempCornerAxis + tempDirections[tempCornerAxis]]) {
|
|
||||||
foundPlace = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert the direction for the next iteration
|
|
||||||
tempDirections[tempCornerAxis] *= -1;
|
|
||||||
|
|
||||||
// Invert the tempCornerAxis
|
|
||||||
tempCornerAxis = tempCornerAxis === 'x' ? 'y' : 'x';
|
|
||||||
|
|
||||||
current++;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cornerAxisPosition;
|
|
||||||
if (foundPlace) {
|
|
||||||
directions = Object.assign({}, directions, tempDirections);
|
|
||||||
cornerAxis = tempCornerAxis;
|
|
||||||
cornerAxisPosition = directions[cornerAxis] < 0 ? 0 : limits[cornerAxis];
|
|
||||||
} else {
|
|
||||||
limits = Object.assign({}, limits, canvasDimensions);
|
|
||||||
|
|
||||||
const toolPositionOnCanvas = cornerstone.pixelToCanvas(element, tool);
|
|
||||||
const renderingInformation = getRenderingInformation(
|
|
||||||
limits,
|
|
||||||
toolPositionOnCanvas
|
|
||||||
);
|
|
||||||
directions = renderingInformation.directions;
|
|
||||||
cornerAxis = renderingInformation.cornerAxis;
|
|
||||||
|
|
||||||
const position = {
|
|
||||||
x: directions.x < 0 ? offset.left : offset.left + canvasWidth,
|
|
||||||
y: directions.y < 0 ? offset.top : offset.top + canvasHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const pixelPosition = cornerstone.pageToPixel(
|
|
||||||
element,
|
|
||||||
position.x,
|
|
||||||
position.y
|
|
||||||
);
|
|
||||||
cornerAxisPosition = pixelPosition[cornerAxis];
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolAxis = cornerAxis === 'x' ? 'y' : 'x';
|
|
||||||
const boxSize = getTextBoxSizeInPixels(element, bounds);
|
|
||||||
|
|
||||||
textBox[cornerAxis] = cornerAxisPosition;
|
|
||||||
textBox[toolAxis] = tool[toolAxis];
|
|
||||||
|
|
||||||
// Adjust the text box position reducing its size from the corner axis
|
|
||||||
const textBoxOffset = getTextBoxOffset(config, cornerAxis, toolAxis, boxSize);
|
|
||||||
textBox[cornerAxis] += textBoxOffset[cornerAxis][directions[cornerAxis]];
|
|
||||||
|
|
||||||
// Preventing the text box from partially going outside the canvas area
|
|
||||||
const topLeft = cornerstone.pixelToCanvas(element, textBox);
|
|
||||||
const bottomRight = {
|
|
||||||
x: topLeft.x + bounds.x,
|
|
||||||
y: topLeft.y + bounds.y,
|
|
||||||
};
|
|
||||||
const canvasBorders = {
|
|
||||||
x0: offset.left,
|
|
||||||
y0: offset.top,
|
|
||||||
x1: offset.left + canvasWidth,
|
|
||||||
y1: offset.top + canvasHeight,
|
|
||||||
};
|
|
||||||
if (topLeft[toolAxis] < 0) {
|
|
||||||
const x = canvasBorders.x0;
|
|
||||||
const y = canvasBorders.y0;
|
|
||||||
const pixelPosition = cornerstone.pageToPixel(element, x, y);
|
|
||||||
textBox[toolAxis] = pixelPosition[toolAxis];
|
|
||||||
} else if (bottomRight[toolAxis] > canvasDimensions[toolAxis]) {
|
|
||||||
const x = canvasBorders.x1 - bounds.x;
|
|
||||||
const y = canvasBorders.y1 - bounds.y;
|
|
||||||
const pixelPosition = cornerstone.pageToPixel(element, x, y);
|
|
||||||
textBox[toolAxis] = pixelPosition[toolAxis];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getBoundingBox, pixelToPage, repositionTextBox };
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import './cornerstone.js';
|
|
||||||
import './parsingUtils.js';
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
import dicomParser from 'dicom-parser';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A small set of utilities to help parsing DICOM element values.
|
|
||||||
* In the future the functionality provided by this library might
|
|
||||||
* be incorporated into dicomParser library.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const parsingUtils = {
|
|
||||||
/**
|
|
||||||
* Check if supplied argument is a valid instance of the dicomParser.DataSet class.
|
|
||||||
* @param data {Object} An instance of the dicomParser.DataSet class.
|
|
||||||
* @returns {Boolean} Returns true if data is a valid instance of the dicomParser.DataSet class.
|
|
||||||
*/
|
|
||||||
isValidDataSet: function(data) {
|
|
||||||
return data instanceof dicomParser.DataSet;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses an element tag according to the 'AT' VR definition.
|
|
||||||
* @param data {Object} An instance of the dicomParser.DataSet class.
|
|
||||||
* @param tag {String} A DICOM tag with in the format xGGGGEEEE.
|
|
||||||
* @returns {String} A string representation of a data element tag or null if the field is not present or data is not long enough.
|
|
||||||
*/
|
|
||||||
attributeTag: function(data, tag) {
|
|
||||||
if (this.isValidDataSet(data) && tag in data.elements) {
|
|
||||||
let element = data.elements[tag];
|
|
||||||
if (element && element.length === 4) {
|
|
||||||
let parser = data.byteArrayParser.readUint16,
|
|
||||||
bytes = data.byteArray,
|
|
||||||
offset = element.dataOffset;
|
|
||||||
return (
|
|
||||||
'x' +
|
|
||||||
(
|
|
||||||
'00000000' +
|
|
||||||
(
|
|
||||||
parser(bytes, offset) * 256 * 256 +
|
|
||||||
parser(bytes, offset + 2)
|
|
||||||
).toString(16)
|
|
||||||
).substr(-8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the string representation of a multi-valued element into an array of strings. If the parser
|
|
||||||
* parameter is passed and is a function, it will be applied to each element of the resulting array.
|
|
||||||
* @param data {Object} An instance of the dicomParser.DataSet class.
|
|
||||||
* @param tag {String} A DICOM tag with in the format xGGGGEEEE.
|
|
||||||
* @param parser {Function} An optional parser function that can be applied to each element of the array.
|
|
||||||
* @returns {Array} An array of floating point numbers or null if the field is not present or data is not long enough.
|
|
||||||
*/
|
|
||||||
multiValue: function(data, tag, parser) {
|
|
||||||
if (this.isValidDataSet(data) && tag in data.elements) {
|
|
||||||
let element = data.elements[tag];
|
|
||||||
if (element && element.length > 0) {
|
|
||||||
let string = dicomParser.readFixedString(
|
|
||||||
data.byteArray,
|
|
||||||
element.dataOffset,
|
|
||||||
element.length
|
|
||||||
);
|
|
||||||
if (typeof string === 'string' && string.length > 0) {
|
|
||||||
if (typeof parser !== 'function') {
|
|
||||||
parser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.split('\\').map(function(value) {
|
|
||||||
value = value.trim();
|
|
||||||
return parser !== null ? parser(value) : value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a string to an array of floats for a multi-valued element.
|
|
||||||
* @param data {Object} An instance of the dicomParser.DataSet class.
|
|
||||||
* @param tag {String} A DICOM tag with in the format xGGGGEEEE.
|
|
||||||
* @returns {Array} An array of floating point numbers or null if the field is not present or data is not long enough.
|
|
||||||
*/
|
|
||||||
floatArray: function(data, tag) {
|
|
||||||
return this.multiValue(data, tag, parseFloat);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,579 +0,0 @@
|
|||||||
import log from '../../log';
|
|
||||||
import { timepointApiDefaultConfig } from './../configuration.js';
|
|
||||||
|
|
||||||
const configuration = {
|
|
||||||
...timepointApiDefaultConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TIMEPOINT_TYPE_NAMES = {
|
|
||||||
prebaseline: 'Pre-Baseline',
|
|
||||||
baseline: 'Baseline',
|
|
||||||
followup: 'Follow-up',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class TimepointApi {
|
|
||||||
static Instance;
|
|
||||||
|
|
||||||
static setConfiguration(config) {
|
|
||||||
Object.assign(configuration, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getConfiguration() {
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(currentTimepointId, options = {}) {
|
|
||||||
if (TimepointApi.Instance) {
|
|
||||||
TimepointApi.Instance.initialize(currentTimepointId, options);
|
|
||||||
return TimepointApi.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialize(currentTimepointId, options);
|
|
||||||
TimepointApi.Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(currentTimepointId, options = {}) {
|
|
||||||
this.currentTimepointId = currentTimepointId;
|
|
||||||
this.comparisonTimepointKey = options.comparisonTimepointKey || 'baseline';
|
|
||||||
this.options = options;
|
|
||||||
this.timepoints = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
onTimepointsUpdated() {
|
|
||||||
if (typeof this.options.onTimepointsUpdated !== 'function') {
|
|
||||||
log.warn('Timepoints update callback is not defined');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.onTimepointsUpdated(Object.assign([], this.timepoints));
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateVisitNumber(timepoint) {
|
|
||||||
// Retrieve all of the relevant follow-up timepoints for this patient
|
|
||||||
const sortedTimepoints = this.timepoints.sort((tp1, tp2) => {
|
|
||||||
return tp1.visitDate > tp2.visitDate ? 1 : -1;
|
|
||||||
});
|
|
||||||
const filteredTimepoints = sortedTimepoints.find(
|
|
||||||
tp =>
|
|
||||||
tp.PatientID === timepoint.PatientID &&
|
|
||||||
tp.timepointType === timepoint.timepointType
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an array of just timepointIds, so we can use indexOf
|
|
||||||
// on it to find the current timepoint's relative position
|
|
||||||
const timepointIds = filteredTimepoints.map(
|
|
||||||
timepoint => timepoint.timepointId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate the index of the current timepoint in the array of all
|
|
||||||
// relevant follow-up timepoints
|
|
||||||
const visitNumber = timepointIds.indexOf(timepoint.timepointId) + 1;
|
|
||||||
|
|
||||||
// If visitNumber is 0, it means that the current timepoint was not in the list
|
|
||||||
if (!visitNumber) {
|
|
||||||
throw new Error(
|
|
||||||
'Current timepoint was not in the list of relevant timepoints?'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visitNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveTimepoints(filter) {
|
|
||||||
const retrievalFn = configuration.dataExchange.retrieve;
|
|
||||||
if (typeof retrievalFn !== 'function') {
|
|
||||||
log.error('Timepoint retrieval function has not been configured.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
retrievalFn(filter)
|
|
||||||
.then(timepointData => {
|
|
||||||
log.info('Timepoint data retrieval');
|
|
||||||
|
|
||||||
timepointData.forEach(timepoint => {
|
|
||||||
const timepointIndex = this.timepoints.findIndex(
|
|
||||||
tp => tp.timepointId === timepoint.timepointId
|
|
||||||
);
|
|
||||||
if (timepointIndex < 0) {
|
|
||||||
this.timepoints.push(timepoint);
|
|
||||||
} else {
|
|
||||||
this.timepoints[timepointIndex] = timepoint;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let others know that the timepoints are updated
|
|
||||||
this.onTimepointsUpdated();
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(reason => {
|
|
||||||
log.error(`Timepoint retrieval function failed: ${reason}`);
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
storeTimepoints() {
|
|
||||||
const storeFn = configuration.dataExchange.store;
|
|
||||||
if (typeof storeFn !== 'function') {
|
|
||||||
log.error('Timepoint store function has not been configured.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Preparing to store timepoints');
|
|
||||||
log.info(JSON.stringify(this.timepoints, null, 2));
|
|
||||||
|
|
||||||
storeFn(this.timepoints).then(() =>
|
|
||||||
log.info('Timepoint storage completed')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
disassociateStudy(timepointIds, StudyInstanceUID) {
|
|
||||||
const disassociateFn = configuration.dataExchange.disassociate;
|
|
||||||
if (typeof disassociateFn !== 'function') {
|
|
||||||
log.error('Study disassociate function has not been configured.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
disassociateFn(timepointIds, StudyInstanceUID).then(() => {
|
|
||||||
log.info('Disassociation completed');
|
|
||||||
|
|
||||||
this.timepoints = [];
|
|
||||||
this.retrieveTimepoints({});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTimepoint(timepointId) {
|
|
||||||
const removeFn = configuration.dataExchange.remove;
|
|
||||||
if (typeof removeFn !== 'function') {
|
|
||||||
log.error('Timepoint remove function has not been configured.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timepointData = {
|
|
||||||
timepointId,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info('Preparing to remove timepoint');
|
|
||||||
log.info(JSON.stringify(timepointData, null, 2));
|
|
||||||
|
|
||||||
removeFn(timepointData).then(() => {
|
|
||||||
log.info('Timepoint removal completed');
|
|
||||||
|
|
||||||
const tpIndex = this.timepoints.findIndex(
|
|
||||||
tp => tp.timepointId === timepointId
|
|
||||||
);
|
|
||||||
if (tpIndex > -1) {
|
|
||||||
this.timepoints.splice(tpIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let others know that the timepoints are updated
|
|
||||||
this.onTimepointsUpdated();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimepoint(timepointId, query) {
|
|
||||||
const updateFn = configuration.dataExchange.update;
|
|
||||||
if (typeof updateFn !== 'function') {
|
|
||||||
log.error('Timepoint update function has not been configured.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timepointData = {
|
|
||||||
timepointId,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info('Preparing to update timepoint');
|
|
||||||
log.info(JSON.stringify(timepointData, null, 2));
|
|
||||||
log.info(JSON.stringify(query, null, 2));
|
|
||||||
|
|
||||||
updateFn(timepointData, query).then(() => {
|
|
||||||
log.info('Timepoint updated completed');
|
|
||||||
|
|
||||||
const tpIndex = this.timepoints.findIndex(
|
|
||||||
tp => tp.timepointId === timepointId
|
|
||||||
);
|
|
||||||
if (tpIndex > -1) {
|
|
||||||
this.timepoints[tpIndex] = Object.assign(
|
|
||||||
{},
|
|
||||||
this.timepoints[tpIndex],
|
|
||||||
query
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let others know that the timepoints are updated
|
|
||||||
this.onTimepointsUpdated();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all timepoints
|
|
||||||
all(filter) {
|
|
||||||
let timepointsToReturn;
|
|
||||||
if (filter) {
|
|
||||||
timepointsToReturn = this.timepoints.filter(filter);
|
|
||||||
} else {
|
|
||||||
timepointsToReturn = this.timepoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
return timepointsToReturn.sort((tp1, tp2) => {
|
|
||||||
return tp1.visitDate < tp2.visitDate ? 1 : -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only the current timepoint
|
|
||||||
current() {
|
|
||||||
return this.timepoints.find(
|
|
||||||
tp => tp.timepointId === this.currentTimepointId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock() {
|
|
||||||
const tpIndex = this.timepoints.findIndex(
|
|
||||||
tp => tp.timepointId === this.currentTimepointId
|
|
||||||
);
|
|
||||||
if (tpIndex < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timepoints[tpIndex] = Object.assign({}, this.timepoints[tpIndex], {
|
|
||||||
locked: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the prior timepoint
|
|
||||||
prior() {
|
|
||||||
const current = this.current();
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.all().find(tp => tp.visitDate < current.visitDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only the current and prior timepoints
|
|
||||||
currentAndPrior() {
|
|
||||||
const timepoints = [];
|
|
||||||
|
|
||||||
const current = this.current();
|
|
||||||
if (current) {
|
|
||||||
timepoints.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prior = this.prior();
|
|
||||||
if (current && prior && prior.timepointId !== current.timepointId) {
|
|
||||||
timepoints.push(prior);
|
|
||||||
}
|
|
||||||
|
|
||||||
return timepoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the current and the comparison timepoints
|
|
||||||
currentAndComparison(comparisonTimepointKey = this.comparisonTimepointKey) {
|
|
||||||
const current = this.current();
|
|
||||||
const comparisonTimepoint = this.comparison(comparisonTimepointKey);
|
|
||||||
const timepoints = [current];
|
|
||||||
|
|
||||||
if (
|
|
||||||
comparisonTimepoint &&
|
|
||||||
!timepoints.find(tp => tp.timepointId === comparisonTimepoint.timepointId)
|
|
||||||
) {
|
|
||||||
timepoints.push(comparisonTimepoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return timepoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if there are 2 or more baseline timepoints before and at the current timepoint, otherwise false
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isRebaseline(timepointId) {
|
|
||||||
const current = timepointId
|
|
||||||
? this.timepoints.find(tp => tp.timepointId === timepointId)
|
|
||||||
: this.current();
|
|
||||||
if (!current) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baselines = this.timepoints.filter(
|
|
||||||
tp => tp.timepointType === 'baseline' && tp.visitDate <= current.visitDate
|
|
||||||
);
|
|
||||||
return baselines.length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the next (closest future) baseline after current timepoint
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
nextBaselineAfterCurrent() {
|
|
||||||
let current = this.current();
|
|
||||||
|
|
||||||
// Get all next timepoints newer than the current timepoint sorted by visitDate ascending
|
|
||||||
const sortedTimepoints = this.timepoints.sort((tp1, tp2) => {
|
|
||||||
return tp1.visitDate > tp2.visitDate ? 1 : -1;
|
|
||||||
});
|
|
||||||
return sortedTimepoints.find(
|
|
||||||
tp => tp.visitDate > current.visitDate && tp.timepointType === 'baseline'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current timepoint id
|
|
||||||
* @param timepointId
|
|
||||||
*/
|
|
||||||
setCurrentTimepointId(timepointId) {
|
|
||||||
this.currentTimepointId = timepointId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the comparison timepoint that overrides the default comparison timepoint (called based on user selection in a viewport)
|
|
||||||
* @param timepoint
|
|
||||||
*/
|
|
||||||
setUserComparison(timepoint) {
|
|
||||||
this.userComparison = timepoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return only the comparison timepoint
|
|
||||||
* @param {String} [comparisonTimepointKey]
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
comparison(comparisonTimepointKey = this.comparisonTimepointKey) {
|
|
||||||
// Return the comparison timepoint set by user if exists
|
|
||||||
if (this.userComparison) {
|
|
||||||
return this.userComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = this.current();
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If current timepoint is prebaseline, the first (closest future) BL after current is comparison regardless of default comparison timepoint
|
|
||||||
if (current.timepointType === 'prebaseline') {
|
|
||||||
const nextBaselineAfterCurrent = this.nextBaselineAfterCurrent();
|
|
||||||
// If there is a next baseline, make it comparison, otherwise comparison is done by default comparison timepoint
|
|
||||||
if (nextBaselineAfterCurrent) {
|
|
||||||
return nextBaselineAfterCurrent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If current timepoint is baseline, the prior is comparison if exists regardless of default comparison timepoint
|
|
||||||
if (current.timepointType === 'baseline') {
|
|
||||||
const prior = this.prior();
|
|
||||||
if (prior) {
|
|
||||||
return prior;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const comparison = this[comparisonTimepointKey]();
|
|
||||||
|
|
||||||
// Do not return a comparison if it would be identical to
|
|
||||||
// the current.
|
|
||||||
if (comparison && comparison.timepointId === current.timepointId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return comparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the latest initial (prebaseline or baseline) timepoint after current and before the next followup timepoint
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
latestInitialTimepointAfterCurrent() {
|
|
||||||
let currentTimepoint = this.current();
|
|
||||||
|
|
||||||
// Skip if the current timepoint is FU since there is no initial timepoint after follow-up
|
|
||||||
if (currentTimepoint.timepointType === 'followup') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all next timepoints newer than the current timepoint sorted by visitDate ascending
|
|
||||||
const sortedTimepoints = this.timepoints.sort((tp1, tp2) => {
|
|
||||||
return tp1.visitDate > tp2.visitDate ? 1 : -1;
|
|
||||||
});
|
|
||||||
const allNextTimepoints = sortedTimepoints.filter(
|
|
||||||
tp => tp.visitDate > currentTimepoint.visitDate
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextFollowupIndex = allNextTimepoints.findIndex(
|
|
||||||
tp => tp.timepointType === 'followup'
|
|
||||||
);
|
|
||||||
const latestInitialBeforeNextFUIndex = nextFollowupIndex - 1;
|
|
||||||
|
|
||||||
if (latestInitialBeforeNextFUIndex < 0) {
|
|
||||||
// There is no FU and all next timepoints are initial, so return the last one
|
|
||||||
return allNextTimepoints[allNextTimepoints.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the latest initial timepoint before the next FU
|
|
||||||
return allNextTimepoints[latestInitialBeforeNextFUIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return timepoint ids of initial timepoints which are prebaseline and baseline
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
initialTimepointIds() {
|
|
||||||
let timepointToCheck = this.current();
|
|
||||||
|
|
||||||
// If the current timepoint is PBL or BL, then get the recent PBL/BL of the current timepoint by its first FU
|
|
||||||
// If it does not exist, then there is no newer initial timepoint, so the current timepoint is used to determine initial timepoint ids
|
|
||||||
if (
|
|
||||||
timepointToCheck.timepointType === 'prebaseline' ||
|
|
||||||
timepointToCheck.timepointType === 'baseline'
|
|
||||||
) {
|
|
||||||
timepointToCheck =
|
|
||||||
this.latestInitialTimepointAfterCurrent() || timepointToCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitDateToCheck = timepointToCheck.visitDate;
|
|
||||||
|
|
||||||
const preBaselineTimepoints =
|
|
||||||
this.timepoints.filter(
|
|
||||||
tp =>
|
|
||||||
tp.timepointType === 'prebaseline' && tp.visitDate <= visitDateToCheck
|
|
||||||
) || [];
|
|
||||||
const preBaselineTimepointIds = preBaselineTimepoints.map(
|
|
||||||
timepoint => timepoint.timepointId
|
|
||||||
);
|
|
||||||
|
|
||||||
const baselineTimepoints =
|
|
||||||
this.timepoints.filter(
|
|
||||||
tp =>
|
|
||||||
tp.timepointType === 'baseline' && tp.visitDate <= visitDateToCheck
|
|
||||||
) || [];
|
|
||||||
const baselineTimepointIds = baselineTimepoints.map(
|
|
||||||
timepoint => timepoint.timepointId
|
|
||||||
);
|
|
||||||
|
|
||||||
return preBaselineTimepointIds.concat(baselineTimepointIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only the baseline timepoint
|
|
||||||
baseline() {
|
|
||||||
const currentVisitDate = this.current().visitDate;
|
|
||||||
return this.all().find(
|
|
||||||
tp => tp.timepointType === 'baseline' && tp.visitDate <= currentVisitDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return only the nadir timepoint. Must be prior to the current timepoint
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
nadir() {
|
|
||||||
const current = this.current();
|
|
||||||
const nadir = this.all().find(
|
|
||||||
tp =>
|
|
||||||
tp.timepointId !== current.timepointId &&
|
|
||||||
tp.timepointKey === 'nadir' &&
|
|
||||||
tp.visitDate <= current.visitDate
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we have found a nadir, return that
|
|
||||||
if (nadir) {
|
|
||||||
return nadir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return the most recent baseline
|
|
||||||
// This should only happen if we are only at FU1,
|
|
||||||
// so the baseline is the nadir.
|
|
||||||
return this.baseline();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only the key timepoints (current, prior, nadir and baseline)
|
|
||||||
key() {
|
|
||||||
const result = [this.current()];
|
|
||||||
const prior = this.prior();
|
|
||||||
const nadir = this.nadir();
|
|
||||||
const baseline = this.baseline();
|
|
||||||
|
|
||||||
const resultIncludes = timepoint =>
|
|
||||||
!!result.find(x => x.timepointId === timepoint.timepointId);
|
|
||||||
|
|
||||||
if (prior && resultIncludes(prior) === false) {
|
|
||||||
result.push(prior);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nadir && resultIncludes(nadir) === false) {
|
|
||||||
result.push(nadir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseline && resultIncludes(baseline) === false) {
|
|
||||||
result.push(baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return only the timepoints for the given study
|
|
||||||
study(StudyInstanceUID) {
|
|
||||||
return this.all().filter(timepoint =>
|
|
||||||
timepoint.studyInstanceUIDs.includes(StudyInstanceUID)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the timepoint's name
|
|
||||||
name(timepoint) {
|
|
||||||
const timepointTypeName = TIMEPOINT_TYPE_NAMES[timepoint.timepointType];
|
|
||||||
|
|
||||||
// Check if this is a Baseline timepoint, if it is, return 'Baseline'
|
|
||||||
if (timepoint.timepointType === 'baseline') {
|
|
||||||
return 'Baseline';
|
|
||||||
} else if (timepoint.visitNumber) {
|
|
||||||
return `${timepointTypeName} ${timepoint.visitNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitNumber = this.calculateVisitNumber(timepoint);
|
|
||||||
|
|
||||||
// Return the timepoint name as 'Follow-up N'
|
|
||||||
return `${timepointTypeName} ${visitNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the timepoint title based on its date
|
|
||||||
title(timepoint) {
|
|
||||||
const timepointName = this.name(timepoint);
|
|
||||||
|
|
||||||
const all = this.all();
|
|
||||||
let index = -1;
|
|
||||||
let currentIndex = null;
|
|
||||||
for (let i = 0; i < all.length; i++) {
|
|
||||||
const currentTimepoint = all[i];
|
|
||||||
|
|
||||||
// Skip the iterations until we can't find the selected timepoint on study list
|
|
||||||
if (this.currentTimepointId === currentTimepoint.timepointId) {
|
|
||||||
currentIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentIndex !== null) {
|
|
||||||
index = currentIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break the loop if reached the timepoint to get the title
|
|
||||||
if (currentTimepoint.timepointId === timepoint.timepointId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
0: ['Current'],
|
|
||||||
1: ['Prior'],
|
|
||||||
};
|
|
||||||
const parenthesis = states[index] || [];
|
|
||||||
const nadir = this.nadir();
|
|
||||||
|
|
||||||
if (nadir && nadir.timepointId === timepoint.timepointId) {
|
|
||||||
parenthesis.push('Nadir');
|
|
||||||
}
|
|
||||||
|
|
||||||
let parenthesisText = '';
|
|
||||||
if (parenthesis.length) {
|
|
||||||
parenthesisText = `(${parenthesis.join(', ')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${timepointName} ${parenthesisText}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import MeasurementApi from './MeasurementApi';
|
|
||||||
import TimepointApi from './TimepointApi';
|
|
||||||
|
|
||||||
export { TimepointApi, MeasurementApi };
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { allTools } from './toolGroups/allTools';
|
|
||||||
import {
|
|
||||||
retrieveMeasurements,
|
|
||||||
storeMeasurements,
|
|
||||||
retrieveTimepoints,
|
|
||||||
storeTimepoints,
|
|
||||||
removeTimepoint,
|
|
||||||
updateTimepoint,
|
|
||||||
disassociateStudy,
|
|
||||||
} from './dataExchange';
|
|
||||||
|
|
||||||
const measurementApiDefaultConfig = {
|
|
||||||
measurementTools: [allTools],
|
|
||||||
newLesions: [
|
|
||||||
{
|
|
||||||
id: 'newTargets',
|
|
||||||
name: 'New Targets',
|
|
||||||
toolGroupId: 'targets',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'newNonTargets',
|
|
||||||
name: 'New Non-Targets',
|
|
||||||
toolGroupId: 'nonTargets',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: retrieveMeasurements,
|
|
||||||
store: storeMeasurements,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const timepointApiDefaultConfig = {
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: retrieveTimepoints,
|
|
||||||
store: storeTimepoints,
|
|
||||||
remove: removeTimepoint,
|
|
||||||
update: updateTimepoint,
|
|
||||||
disassociate: disassociateStudy,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export { measurementApiDefaultConfig, timepointApiDefaultConfig };
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import { BaseCriterion } from './criteria/BaseCriterion';
|
|
||||||
import * as initialCriteria from './criteria';
|
|
||||||
import Ajv from 'ajv';
|
|
||||||
|
|
||||||
const Criteria = Object.assign({}, initialCriteria);
|
|
||||||
|
|
||||||
export class CriteriaEvaluator {
|
|
||||||
constructor(criteriaObject) {
|
|
||||||
const criteriaValidator = this.getCriteriaValidator();
|
|
||||||
this.criteria = [];
|
|
||||||
|
|
||||||
if (!criteriaValidator(criteriaObject)) {
|
|
||||||
let message = '';
|
|
||||||
criteriaValidator.errors.forEach(error => {
|
|
||||||
message += `\noptions${error.dataPath} ${error.message}`;
|
|
||||||
});
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(criteriaObject).forEach(criterionkey => {
|
|
||||||
const optionsObject = criteriaObject[criterionkey];
|
|
||||||
const Criterion = Criteria[`${criterionkey}Criterion`];
|
|
||||||
const optionsArray =
|
|
||||||
optionsObject instanceof Array ? optionsObject : [optionsObject];
|
|
||||||
optionsArray.forEach(options =>
|
|
||||||
this.criteria.push(new Criterion(options, criterionkey))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxTargets(newTarget = false) {
|
|
||||||
let result = 0;
|
|
||||||
this.criteria.forEach(criterion => {
|
|
||||||
const newTargetMatch = newTarget === !!criterion.options.newTarget;
|
|
||||||
if (criterion instanceof Criteria.MaxTargetsCriterion && newTargetMatch) {
|
|
||||||
const { limit } = criterion.options;
|
|
||||||
if (limit > result) {
|
|
||||||
result = limit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCriteriaValidator() {
|
|
||||||
if (CriteriaEvaluator.criteriaValidator) {
|
|
||||||
return CriteriaEvaluator.criteriaValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
properties: {},
|
|
||||||
definitions: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(Criteria).forEach(key => {
|
|
||||||
const Criterion = Criteria[key];
|
|
||||||
if (Criterion.prototype instanceof BaseCriterion) {
|
|
||||||
const criterionkey = key.replace(/Criterion$/, '');
|
|
||||||
const criterionDefinition = `#/definitions/${criterionkey}`;
|
|
||||||
|
|
||||||
schema.definitions[criterionkey] = Criteria[`${criterionkey}Schema`];
|
|
||||||
schema.properties[criterionkey] = {
|
|
||||||
oneOf: [
|
|
||||||
{ $ref: criterionDefinition },
|
|
||||||
{
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
$ref: criterionDefinition,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CriteriaEvaluator.criteriaValidator = new Ajv().compile(schema);
|
|
||||||
return CriteriaEvaluator.criteriaValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const nonconformities = [];
|
|
||||||
this.criteria.forEach(criterion => {
|
|
||||||
const criterionResult = criterion.evaluate(data);
|
|
||||||
if (!criterionResult.passed) {
|
|
||||||
nonconformities.push(criterionResult);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return nonconformities;
|
|
||||||
}
|
|
||||||
|
|
||||||
static setCriterion(criterionKey, criterionDefinitions) {
|
|
||||||
Criteria[criterionKey] = criterionDefinitions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
export class BaseCriterion {
|
|
||||||
constructor(options, criterionName) {
|
|
||||||
this.options = options;
|
|
||||||
this.criterionName = criterionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateResponse(message, measurements) {
|
|
||||||
const passed = !message;
|
|
||||||
const isGlobal = !measurements || !measurements.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
passed,
|
|
||||||
isGlobal,
|
|
||||||
message,
|
|
||||||
measurements,
|
|
||||||
criterionName: this.criterionName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewTargetNumbers(data) {
|
|
||||||
const { options } = this;
|
|
||||||
const baselineMeasurementNumbers = [];
|
|
||||||
const newTargetNumbers = new Set();
|
|
||||||
|
|
||||||
if (options.newTarget) {
|
|
||||||
data.targets.forEach(target => {
|
|
||||||
const { measurementNumber } = target.measurement;
|
|
||||||
if (target.timepoint.timepointType === 'baseline') {
|
|
||||||
baselineMeasurementNumbers.push(measurementNumber);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
data.targets.forEach(target => {
|
|
||||||
const { measurementNumber } = target.measurement;
|
|
||||||
if (target.timepoint.timepointType === 'followup') {
|
|
||||||
if (!baselineMeasurementNumbers.includes(measurementNumber)) {
|
|
||||||
newTargetNumbers.add(measurementNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTargetNumbers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const LocationSchema = {
|
|
||||||
type: 'object',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* LocationCriterion
|
|
||||||
* Check if the there are non-target measurements with response different than "present" on baseline
|
|
||||||
*/
|
|
||||||
export class LocationCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const items = data.targets.concat(data.nonTargets);
|
|
||||||
const measurements = [];
|
|
||||||
let message;
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const measurement = item.measurement;
|
|
||||||
|
|
||||||
if (!measurement.location) {
|
|
||||||
measurements.push(measurement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (measurements.length) {
|
|
||||||
message = 'All measurements should have a location';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const MaxTargetsSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
limit: {
|
|
||||||
label: 'Max targets allowed in study',
|
|
||||||
type: 'integer',
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
newTarget: {
|
|
||||||
label: 'Flag to evaluate only new targets',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
locationIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements with the specified locations',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
locationNotIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements without the specified locations',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
isNodal: {
|
|
||||||
label: 'Filter to evaluate only nodal or extranodal measurements',
|
|
||||||
type: 'boolean'
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
label: 'Message to be displayed in case of nonconformity',
|
|
||||||
type: 'string',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['limit'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/* MaxTargetsCriterion
|
|
||||||
* Check if the number of target measurements exceeded the limit allowed
|
|
||||||
* Options:
|
|
||||||
* limit: Max targets allowed in study
|
|
||||||
* newTarget: Flag to evaluate only new targets (must be evaluated on both)
|
|
||||||
* locationIn: Filter to evaluate only measurements with the specified locations
|
|
||||||
* locationNotIn: Filter to evaluate only measurements without the specified locations
|
|
||||||
* isNodal: Filter to evaluate only nodal or extranodal measurements
|
|
||||||
* message: Message to be displayed in case of nonconformity
|
|
||||||
*/
|
|
||||||
export class MaxTargetsCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const { options } = this;
|
|
||||||
|
|
||||||
const newTargetNumbers = this.getNewTargetNumbers(data);
|
|
||||||
const measurementNumbers = [];
|
|
||||||
data.targets.forEach(target => {
|
|
||||||
const { location, measurementNumber, isSplitLesion, isNodal } = target.measurement;
|
|
||||||
|
|
||||||
if (isSplitLesion)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (typeof isNodal === 'boolean' && typeof options.isNodal === 'boolean' && options.isNodal !== isNodal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (options.newTarget && !newTargetNumbers.has(measurementNumber))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (options.locationIn && options.locationIn.indexOf(location) === -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (options.locationNotIn && options.locationNotIn.indexOf(location) > -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
measurementNumbers.push(measurementNumber);
|
|
||||||
});
|
|
||||||
|
|
||||||
let lesionType = '';
|
|
||||||
if (typeof options.isNodal === 'boolean') {
|
|
||||||
lesionType = options.isNodal ? 'nodal ' : 'extranodal ';
|
|
||||||
}
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if (measurementNumbers.length > options.limit) {
|
|
||||||
const increment = options.newTarget ? 'new ' : '';
|
|
||||||
const plural = options.limit === 1 ? '' : 's';
|
|
||||||
const amount = options.limit === 0 ? '' : `more than ${options.limit}`;
|
|
||||||
message =
|
|
||||||
options.message ||
|
|
||||||
`The study should not have ${amount} ${increment}${lesionType}target${plural}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const MaxTargetsPerOrganSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
limit: {
|
|
||||||
label: 'Max targets allowed per organ',
|
|
||||||
type: 'integer',
|
|
||||||
minimum: 1,
|
|
||||||
},
|
|
||||||
newTarget: {
|
|
||||||
label: 'Flag to evaluate only new targets',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
isNodal: {
|
|
||||||
label: 'Filter to evaluate only nodal or extranodal measurements',
|
|
||||||
type: 'boolean'
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
label: 'Message to be displayed in case of nonconformity',
|
|
||||||
type: 'string',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['limit'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* MaxTargetsPerOrganCriterion
|
|
||||||
* Check if the number of target measurements per organ exceeded the limit allowed
|
|
||||||
* Options:
|
|
||||||
* limit: Max targets allowed in study
|
|
||||||
* newTarget: Flag to evaluate only new targets (must be evaluated on both)
|
|
||||||
* isNodal: Filter to evaluate only nodal or extranodal measurements
|
|
||||||
* message: Message to be displayed in case of nonconformity
|
|
||||||
*/
|
|
||||||
export class MaxTargetsPerOrganCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const { options } = this;
|
|
||||||
const targetsPerOrgan = {};
|
|
||||||
let measurements = [];
|
|
||||||
|
|
||||||
const newTargetNumbers = this.getNewTargetNumbers(data);
|
|
||||||
data.targets.forEach(target => {
|
|
||||||
const { measurement } = target;
|
|
||||||
const { location, measurementNumber, isSplitLesion, isNodal } = measurement;
|
|
||||||
|
|
||||||
if (isSplitLesion)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (typeof isNodal === 'boolean' && typeof options.isNodal === 'boolean' && options.isNodal !== isNodal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!targetsPerOrgan[location]) {
|
|
||||||
targetsPerOrgan[location] = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.newTarget || newTargetNumbers.has(measurementNumber)) {
|
|
||||||
targetsPerOrgan[location].add(measurementNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetsPerOrgan[location].size > options.limit) {
|
|
||||||
measurements.push(measurement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if (measurements.length) {
|
|
||||||
const increment = options.newTarget ? 'new ' : '';
|
|
||||||
message =
|
|
||||||
options.message ||
|
|
||||||
`Each organ should not have more than ${
|
|
||||||
options.limit
|
|
||||||
} ${increment}targets.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const MeasurementsLengthSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
longAxis: {
|
|
||||||
label: 'Minimum length of long axis',
|
|
||||||
type: 'number',
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
shortAxis: {
|
|
||||||
label: 'Minimum length of short axis',
|
|
||||||
type: 'number',
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
longAxisSliceThicknessMultiplier: {
|
|
||||||
label: 'Length of long axis multiplier',
|
|
||||||
type: 'number',
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
shortAxisSliceThicknessMultiplier: {
|
|
||||||
label: 'Length of short axis multiplier',
|
|
||||||
type: 'number',
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
modalityIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements with the specified modalities',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
modalityNotIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements without the specified modalities',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
locationIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements with the specified locations',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
locationNotIn: {
|
|
||||||
label:
|
|
||||||
'Filter to evaluate only measurements without the specified locations',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
isNodal: {
|
|
||||||
label: 'Filter to evaluate only nodal or extranodal measurements',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
label: 'Message to be displayed in case of nonconformity',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
anyOf: [
|
|
||||||
{ required: ['message', 'longAxis'] },
|
|
||||||
{ required: ['message', 'shortAxis'] },
|
|
||||||
{ required: ['message', 'longAxisSliceThicknessMultiplier'] },
|
|
||||||
{ required: ['message', 'shortAxisSliceThicknessMultiplier'] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* MeasurementsLengthCriterion
|
|
||||||
* Check the measurements of all bidirectional tools based on
|
|
||||||
* short axis, long axis, modalities, location and slice thickness
|
|
||||||
* Options:
|
|
||||||
* longAxis: Minimum length of long axis
|
|
||||||
* shortAxis: Minimum length of short axis
|
|
||||||
* longAxisSliceThicknessMultiplier: Length of long axis multiplier
|
|
||||||
* shortAxisSliceThicknessMultiplier: Length of short axis multiplier
|
|
||||||
* modalityIn: Filter to evaluate only measurements with the specified modalities
|
|
||||||
* modalityNotIn: Filter to evaluate only measurements without the specified modalities
|
|
||||||
* locationIn: Filter to evaluate only measurements with the specified locations
|
|
||||||
* locationNotIn: Filter to evaluate only measurements without the specified locations
|
|
||||||
* isNodal: Filter to evaluate only nodal or extranodal measurements
|
|
||||||
* message: Message to be displayed in case of nonconformity
|
|
||||||
*/
|
|
||||||
export class MeasurementsLengthCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
let message;
|
|
||||||
let measurements = [];
|
|
||||||
const { options } = this;
|
|
||||||
const longMultiplier = options.longAxisSliceThicknessMultiplier;
|
|
||||||
const shortMultiplier = options.shortAxisSliceThicknessMultiplier;
|
|
||||||
|
|
||||||
data.targets.forEach(item => {
|
|
||||||
const { metadata, measurement } = item;
|
|
||||||
const { location } = measurement;
|
|
||||||
|
|
||||||
let { longestDiameter, shortestDiameter, isNodal } = measurement;
|
|
||||||
if (measurement.childToolsCount) {
|
|
||||||
const child = measurement.bidirectional;
|
|
||||||
longestDiameter = (child && child.longestDiameter) || 0;
|
|
||||||
shortestDiameter = (child && child.shortestDiameter) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { SliceThickness } = metadata;
|
|
||||||
|
|
||||||
const Modality = metadata.getTagValue('Modality') || '';
|
|
||||||
|
|
||||||
// Stop here if the measurement does not match the Modality and location filters
|
|
||||||
if (
|
|
||||||
typeof isNodal === 'boolean' &&
|
|
||||||
typeof options.isNodal === 'boolean' &&
|
|
||||||
options.isNodal !== isNodal
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
if (options.locationIn && options.locationIn.indexOf(location) === -1)
|
|
||||||
return;
|
|
||||||
if (options.modalityIn && options.modalityIn.indexOf(Modality) === -1)
|
|
||||||
return;
|
|
||||||
if (options.locationNotIn && options.locationNotIn.indexOf(location) > -1)
|
|
||||||
return;
|
|
||||||
if (options.modalityNotIn && options.modalityNotIn.indexOf(Modality) > -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Check the measurement length
|
|
||||||
const failed =
|
|
||||||
(options.longAxis && longestDiameter < options.longAxis) ||
|
|
||||||
(options.shortAxis && shortestDiameter < options.shortAxis) ||
|
|
||||||
(longMultiplier &&
|
|
||||||
!isNaN(SliceThickness) &&
|
|
||||||
longestDiameter < longMultiplier * SliceThickness) ||
|
|
||||||
(shortMultiplier &&
|
|
||||||
!isNaN(SliceThickness) &&
|
|
||||||
shortestDiameter < shortMultiplier * SliceThickness);
|
|
||||||
|
|
||||||
// Mark this measurement as invalid if some of the checks have failed
|
|
||||||
if (failed) {
|
|
||||||
measurements.push(measurement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use the options' message if some measurement is invalid
|
|
||||||
if (measurements.length) {
|
|
||||||
message = options.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const ModalitySchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
method: {
|
|
||||||
label: 'Specify if it\'s goinig to "allow" or "deny" the modalities',
|
|
||||||
type: 'string',
|
|
||||||
enum: ['allow', 'deny'],
|
|
||||||
},
|
|
||||||
measurementTypes: {
|
|
||||||
label: 'List of measurement types that will be evaluated',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
modalities: {
|
|
||||||
label: 'List of allowed/denied modalities',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
uniqueItems: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['method', 'modalities'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ModalityCriteria
|
|
||||||
* Check if a Modality is allowed or denied
|
|
||||||
* Options:
|
|
||||||
* method (string): Specify if it\'s goinig to "allow" or "deny" the modalities
|
|
||||||
* measurementTypes (string[]): List of measurement types that will be evaluated
|
|
||||||
* modalities (string[]): List of allowed/denied modalities
|
|
||||||
*/
|
|
||||||
export class ModalityCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const measurementTypes = this.options.measurementTypes || ['targets'];
|
|
||||||
const modalitiesSet = new Set(this.options.modalities);
|
|
||||||
const validationMethod = this.options.method;
|
|
||||||
const measurements = [];
|
|
||||||
const invalidModalities = new Set();
|
|
||||||
let message;
|
|
||||||
|
|
||||||
measurementTypes.forEach(measurementType => {
|
|
||||||
const items = data[measurementType];
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const { measurement, metadata } = item;
|
|
||||||
const Modality = metadata.getTagValue('Modality') || '';
|
|
||||||
|
|
||||||
if (
|
|
||||||
(validationMethod === 'allow' && !modalitiesSet.has(Modality)) ||
|
|
||||||
(validationMethod === 'deny' && modalitiesSet.has(Modality))
|
|
||||||
) {
|
|
||||||
measurements.push(measurement);
|
|
||||||
invalidModalities.add(Modality);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (measurements.length) {
|
|
||||||
const uniqueModalities = Array.from(invalidModalities);
|
|
||||||
const uniqueModalitiesText = uniqueModalities.join(', ');
|
|
||||||
const modalityText =
|
|
||||||
uniqueModalities.length > 1 ? 'modalities' : 'Modality';
|
|
||||||
|
|
||||||
message = `The ${modalityText} ${uniqueModalitiesText} should not be used as a method of measurement`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const NonTargetResponseSchema = {
|
|
||||||
type: 'object',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* NonTargetResponseCriterion
|
|
||||||
* Check if the there are non-target measurements with response different than "present" on baseline
|
|
||||||
*/
|
|
||||||
export class NonTargetResponseCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const items = data.nonTargets;
|
|
||||||
const measurements = [];
|
|
||||||
let message;
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const measurement = item.measurement;
|
|
||||||
const response = (measurement.response || '').toLowerCase();
|
|
||||||
|
|
||||||
if (response !== 'present') {
|
|
||||||
measurements.push(measurement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (measurements.length) {
|
|
||||||
message = 'Non-targets can only be assessed as "present"';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { BaseCriterion } from './BaseCriterion';
|
|
||||||
|
|
||||||
export const TargetTypeSchema = {
|
|
||||||
type: 'object',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* TargetTypeCriterion
|
|
||||||
* Check if the there are non-bidirectional target measurements on baseline
|
|
||||||
*/
|
|
||||||
export class TargetTypeCriterion extends BaseCriterion {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(data) {
|
|
||||||
const items = data.targets;
|
|
||||||
const measurements = [];
|
|
||||||
let message;
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const measurement = item.measurement;
|
|
||||||
|
|
||||||
if (
|
|
||||||
measurement.toolType !== 'Bidirectional' &&
|
|
||||||
!measurement.bidirectional
|
|
||||||
) {
|
|
||||||
measurements.push(measurement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (measurements.length) {
|
|
||||||
message =
|
|
||||||
'Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateResponse(message, measurements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export * from './Location';
|
|
||||||
export * from './MaxTargetsPerOrgan';
|
|
||||||
export * from './MaxTargets';
|
|
||||||
export * from './MeasurementsLength';
|
|
||||||
export * from './Modality';
|
|
||||||
export * from './NonTargetResponse';
|
|
||||||
export * from './TargetType';
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import * as recistEvaluation from './recist.json';
|
|
||||||
|
|
||||||
export const recist11 = recistEvaluation;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"both": {
|
|
||||||
"Location": {}
|
|
||||||
},
|
|
||||||
"baseline": {
|
|
||||||
"TargetType": {},
|
|
||||||
"MaxTargetsPerOrgan": {
|
|
||||||
"limit": 2
|
|
||||||
},
|
|
||||||
"MaxTargets": {
|
|
||||||
"limit": 5
|
|
||||||
},
|
|
||||||
"MeasurementsLength": [
|
|
||||||
{
|
|
||||||
"longAxis": 10,
|
|
||||||
"longAxisSliceThicknessMultiplier": 2,
|
|
||||||
"modalityIn": ["CT", "MR"],
|
|
||||||
"locationNotIn": ["Lymph Node"],
|
|
||||||
"message": "Extranodal lesions must be >= 10mm long axis AND >= double the acquisition slice thickness by CT and MR"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortAxis": 20,
|
|
||||||
"longAxis": 20,
|
|
||||||
"modalityIn": ["PX", "XA"],
|
|
||||||
"locationNotIn": ["Lymph Node"],
|
|
||||||
"message": "Extranodal lesions must be >= 20mm on chest x-ray (although x-rays rarely used for clinical trial assessment)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortAxis": 15,
|
|
||||||
"shortAxisSliceThicknessMultiplier": 2,
|
|
||||||
"modalityIn": ["CT", "MR"],
|
|
||||||
"locationIn": ["Lymph Node"],
|
|
||||||
"message": "Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"followup": {}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import ConformanceCriteria from './ConformanceCriteria';
|
|
||||||
|
|
||||||
export { ConformanceCriteria };
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import log from '../log';
|
|
||||||
|
|
||||||
export const retrieveMeasurements = (PatientID, timepointIds) => {
|
|
||||||
log.error('retrieveMeasurements');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const storeMeasurements = (measurementData, timepointIds) => {
|
|
||||||
log.error('storeMeasurements');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const retrieveTimepoints = filter => {
|
|
||||||
log.error('retrieveTimepoints');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const storeTimepoints = timepointData => {
|
|
||||||
log.error('storeTimepoints');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTimepoint = (timepointData, query) => {
|
|
||||||
log.error('updateTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeTimepoint = timepointId => {
|
|
||||||
log.error('removeTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const disassociateStudy = (timepointIds, StudyInstanceUID) => {
|
|
||||||
log.error('disassociateStudy');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import * as tools from './tools';
|
|
||||||
|
|
||||||
import { MeasurementApi, TimepointApi } from './classes';
|
|
||||||
import { ConformanceCriteria } from './conformance';
|
|
||||||
import MeasurementHandlers from './measurementHandlers';
|
|
||||||
import getDescription from './lib/getDescription';
|
|
||||||
import getImageAttributes from './lib/getImageAttributes';
|
|
||||||
import getImageIdForImagePath from './lib/getImageIdForImagePath';
|
|
||||||
import getLabel from './lib/getLabel';
|
|
||||||
import ltTools from './ltTools';
|
|
||||||
|
|
||||||
const measurements = {
|
|
||||||
TimepointApi,
|
|
||||||
MeasurementApi,
|
|
||||||
ConformanceCriteria,
|
|
||||||
MeasurementHandlers,
|
|
||||||
ltTools,
|
|
||||||
tools,
|
|
||||||
getLabel,
|
|
||||||
getDescription,
|
|
||||||
getImageAttributes,
|
|
||||||
getImageIdForImagePath,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default measurements;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export default function(measurement) {
|
|
||||||
return measurement.description;
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
|
|
||||||
export default function(element) {
|
|
||||||
// Get the Cornerstone imageId
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
const imageId = enabledElement.image.imageId;
|
|
||||||
|
|
||||||
// Get StudyInstanceUID & PatientID
|
|
||||||
const {
|
|
||||||
StudyInstanceUID,
|
|
||||||
PatientID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
} = cornerstone.metaData.get('instance', imageId);
|
|
||||||
|
|
||||||
const splitImageId = imageId.split('&frame');
|
|
||||||
const frameIndex =
|
|
||||||
splitImageId[1] !== undefined ? Number(splitImageId[1]) : 0;
|
|
||||||
|
|
||||||
const imagePath = [
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameIndex,
|
|
||||||
].join('_');
|
|
||||||
|
|
||||||
return {
|
|
||||||
PatientID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameIndex,
|
|
||||||
imagePath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import studyMetadataManager from '../../utils/studyMetadataManager';
|
|
||||||
|
|
||||||
export default function(imagePath, thumbnail = false) {
|
|
||||||
const [
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameIndex,
|
|
||||||
] = imagePath.split('_');
|
|
||||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
|
||||||
const series = studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
|
||||||
const instance = series.getInstanceByUID(SOPInstanceUID);
|
|
||||||
return instance.getImageId(frameIndex, thumbnail);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
export default function(measurement) {
|
|
||||||
if (!measurement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (measurement.toolType) {
|
|
||||||
case 'Bidirectional':
|
|
||||||
case 'TargetCR':
|
|
||||||
case 'TargetNE':
|
|
||||||
case 'TargetUN':
|
|
||||||
return `Target ${measurement.lesionNamingNumber}`;
|
|
||||||
case 'NonTarget':
|
|
||||||
return `Non-Target ${measurement.lesionNamingNumber}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { targets } from './toolGroups/targets';
|
|
||||||
import { nonTargets } from './toolGroups/nonTargets';
|
|
||||||
import { temp } from './toolGroups/temp';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
const ltTools = cloneDeep([targets, nonTargets, temp]);
|
|
||||||
|
|
||||||
ltTools.forEach(toolGroup => {
|
|
||||||
toolGroup.childTools.forEach(tool => {
|
|
||||||
tool.toolGroup = toolGroup.id;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ltTools;
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
import user from '../../user';
|
|
||||||
import getImageAttributes from '../lib/getImageAttributes';
|
|
||||||
import getLabel from '../lib/getLabel';
|
|
||||||
|
|
||||||
export default function({ eventData, tool, toolGroupId, toolGroup }) {
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { measurementData } = eventData;
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[tool.parentTool];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
// Stop here if there's no measurement data or if it was cancelled
|
|
||||||
if (!measurementData || measurementData.cancelled) return;
|
|
||||||
|
|
||||||
log.info('CornerstoneToolsMeasurementAdded');
|
|
||||||
|
|
||||||
const imageAttributes = getImageAttributes(eventData.element);
|
|
||||||
|
|
||||||
const additionalProperties = Object.assign(imageAttributes, {
|
|
||||||
userId: user.getUserId(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const childMeasurement = Object.assign(
|
|
||||||
{},
|
|
||||||
measurementData,
|
|
||||||
additionalProperties
|
|
||||||
);
|
|
||||||
|
|
||||||
const parentMeasurement = collection.find(
|
|
||||||
t =>
|
|
||||||
t.toolType === tool.parentTool &&
|
|
||||||
t.PatientID === imageAttributes.PatientID &&
|
|
||||||
t[tool.attribute] === null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if a measurement to fit this child tool already exists
|
|
||||||
if (parentMeasurement) {
|
|
||||||
const key = tool.attribute;
|
|
||||||
|
|
||||||
// Add the createdAt attribute
|
|
||||||
childMeasurement.createdAt = new Date();
|
|
||||||
|
|
||||||
// Update the parent measurement
|
|
||||||
parentMeasurement[key] = childMeasurement;
|
|
||||||
parentMeasurement.childToolsCount =
|
|
||||||
(parentMeasurement.childToolsCount || 0) + 1;
|
|
||||||
measurementApi.updateMeasurement(tool.parentTool, parentMeasurement);
|
|
||||||
|
|
||||||
// Update the measurementData ID and lesionNamingNumber
|
|
||||||
measurementData._id = parentMeasurement._id;
|
|
||||||
measurementData.lesionNamingNumber = parentMeasurement.lesionNamingNumber;
|
|
||||||
} else {
|
|
||||||
const measurement = {
|
|
||||||
toolType: tool.parentTool,
|
|
||||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
|
||||||
userId: user.getUserId(),
|
|
||||||
PatientID: imageAttributes.PatientID,
|
|
||||||
StudyInstanceUID: imageAttributes.StudyInstanceUID,
|
|
||||||
};
|
|
||||||
|
|
||||||
measurement[tool.attribute] = Object.assign(
|
|
||||||
{},
|
|
||||||
measurementData,
|
|
||||||
additionalProperties
|
|
||||||
);
|
|
||||||
|
|
||||||
const addedMeasurement = measurementApi.addMeasurement(
|
|
||||||
tool.parentTool,
|
|
||||||
measurement
|
|
||||||
);
|
|
||||||
Object.assign(measurementData, addedMeasurement);
|
|
||||||
}
|
|
||||||
|
|
||||||
const measurementLabel = getLabel(measurementData);
|
|
||||||
if (measurementLabel) {
|
|
||||||
measurementData.labels = [measurementLabel];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is very hacky, but will work for now
|
|
||||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
||||||
cornerstone.updateImage(enabledElement.element);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Notify about the last activated measurement
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
|
|
||||||
export default function({ eventData, tool, toolGroupId, toolGroup }) {
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { measurementData } = eventData;
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[tool.parentTool];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
log.info('CornerstoneToolsMeasurementModified');
|
|
||||||
|
|
||||||
const measurement = collection.find(t => t._id === measurementData._id);
|
|
||||||
let childMeasurement = measurement && measurement[tool.attribute];
|
|
||||||
|
|
||||||
// Stop here if the measurement is already deleted
|
|
||||||
if (!childMeasurement) return;
|
|
||||||
|
|
||||||
childMeasurement = Object.assign(childMeasurement, measurementData);
|
|
||||||
childMeasurement.viewport = cornerstone.getViewport(eventData.element);
|
|
||||||
|
|
||||||
// Update the parent measurement
|
|
||||||
measurement[tool.attribute] = childMeasurement;
|
|
||||||
measurementApi.updateMeasurement(tool.parentTool, measurement);
|
|
||||||
|
|
||||||
// TODO: Notify about the last activated measurement
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
|
|
||||||
export default function({ eventData, tool, toolGroupId, toolGroup }) {
|
|
||||||
log.info('CornerstoneToolsMeasurementRemoved');
|
|
||||||
const { measurementData } = eventData;
|
|
||||||
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[tool.parentTool];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
const measurementIndex = collection.findIndex(
|
|
||||||
t => t._id === measurementData._id
|
|
||||||
);
|
|
||||||
const measurement =
|
|
||||||
measurementIndex > -1 ? collection[measurementIndex] : null;
|
|
||||||
|
|
||||||
// Stop here if the measurement is already gone or never existed
|
|
||||||
if (!measurement) return;
|
|
||||||
|
|
||||||
if (measurement.childToolsCount === 1) {
|
|
||||||
// Remove the measurement
|
|
||||||
collection.splice(measurementIndex, 1);
|
|
||||||
measurementApi.onMeasurementRemoved(tool.parentTool, measurement);
|
|
||||||
} else {
|
|
||||||
// Update the measurement
|
|
||||||
measurement[tool.attribute] = null;
|
|
||||||
measurement.childToolsCount = (measurement.childToolsCount || 0) - 1;
|
|
||||||
measurementApi.updateMeasurement(tool.parentTool, measurement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is very hacky, but will work for now
|
|
||||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
||||||
cornerstone.updateImage(enabledElement.element);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
import user from '../../user';
|
|
||||||
import getImageAttributes from '../lib/getImageAttributes';
|
|
||||||
import getLabel from '../lib/getLabel';
|
|
||||||
|
|
||||||
export default function handleSingleMeasurementAdded({ eventData, tool }) {
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { measurementData, toolType } = eventData;
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[toolType];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
// Stop here if there's no measurement data or if it was cancelled
|
|
||||||
if (!measurementData || measurementData.cancelled) return;
|
|
||||||
|
|
||||||
log.info('CornerstoneToolsMeasurementAdded');
|
|
||||||
|
|
||||||
const imageAttributes = getImageAttributes(eventData.element);
|
|
||||||
const measurement = Object.assign({}, measurementData, imageAttributes, {
|
|
||||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
|
||||||
userId: user.getUserId(),
|
|
||||||
toolType,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addedMeasurement = measurementApi.addMeasurement(toolType, measurement);
|
|
||||||
Object.assign(measurementData, addedMeasurement);
|
|
||||||
|
|
||||||
const measurementLabel = getLabel(measurementData);
|
|
||||||
if (measurementLabel) {
|
|
||||||
measurementData.labels = [measurementLabel];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is very hacky, but will work for now
|
|
||||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
||||||
cornerstone.updateImage(enabledElement.element);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Notify about the last activated measurement
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
|
|
||||||
export default function({ eventData, tool, toolGroupId, toolGroup }) {
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { measurementData, toolType } = eventData;
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[toolType];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
log.info('CornerstoneToolsMeasurementModified');
|
|
||||||
let measurement = collection.find(t => t._id === measurementData._id);
|
|
||||||
|
|
||||||
// Stop here if the measurement is already deleted
|
|
||||||
if (!measurement) return;
|
|
||||||
|
|
||||||
measurement = Object.assign(measurement, measurementData);
|
|
||||||
measurement.viewport = cornerstone.getViewport(eventData.element);
|
|
||||||
|
|
||||||
measurementApi.updateMeasurement(toolType, measurement);
|
|
||||||
|
|
||||||
// TODO: Notify about the last activated measurement
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import log from '../../log';
|
|
||||||
|
|
||||||
export default function handleSingleMeasurementRemoved({
|
|
||||||
eventData,
|
|
||||||
tool,
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
}) {
|
|
||||||
log.info('CornerstoneToolsMeasurementRemoved');
|
|
||||||
const { measurementData, toolType } = eventData;
|
|
||||||
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
if (!measurementApi) {
|
|
||||||
log.warn('Measurement API is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const collection = measurementApi.tools[toolType];
|
|
||||||
|
|
||||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
|
||||||
if (!collection) return;
|
|
||||||
|
|
||||||
const measurementTypeId = measurementApi.toolsGroupsMap[toolType];
|
|
||||||
const measurement = collection.find(t => t._id === measurementData._id);
|
|
||||||
|
|
||||||
// Stop here if the measurement is already gone or never existed
|
|
||||||
if (!measurement) return;
|
|
||||||
|
|
||||||
// Remove all the measurements with the given type and number
|
|
||||||
const { lesionNamingNumber, timepointId } = measurement;
|
|
||||||
measurementApi.deleteMeasurements(toolType, measurementTypeId, {
|
|
||||||
lesionNamingNumber,
|
|
||||||
timepointId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This is very hacky, but will work for now
|
|
||||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
||||||
cornerstone.updateImage(enabledElement.element);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (MeasurementApi.isToolIncluded(tool)) {
|
|
||||||
// TODO: Notify that viewer suffered changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
import { MeasurementApi } from '../classes';
|
|
||||||
import handleSingleMeasurementAdded from './handleSingleMeasurementAdded';
|
|
||||||
import handleChildMeasurementAdded from './handleChildMeasurementAdded';
|
|
||||||
import handleSingleMeasurementModified from './handleSingleMeasurementModified';
|
|
||||||
import handleChildMeasurementModified from './handleChildMeasurementModified';
|
|
||||||
import handleSingleMeasurementRemoved from './handleSingleMeasurementRemoved';
|
|
||||||
import handleChildMeasurementRemoved from './handleChildMeasurementRemoved';
|
|
||||||
|
|
||||||
const getEventData = event => {
|
|
||||||
const eventData = event.detail;
|
|
||||||
if (eventData.toolName) {
|
|
||||||
eventData.toolType = eventData.toolName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MeasurementHandlers = {
|
|
||||||
handleSingleMeasurementAdded,
|
|
||||||
handleChildMeasurementAdded,
|
|
||||||
handleSingleMeasurementModified,
|
|
||||||
handleChildMeasurementModified,
|
|
||||||
handleSingleMeasurementRemoved,
|
|
||||||
handleChildMeasurementRemoved,
|
|
||||||
|
|
||||||
onAdded(event) {
|
|
||||||
const eventData = getEventData(event);
|
|
||||||
const { toolType } = eventData;
|
|
||||||
const {
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
tool,
|
|
||||||
} = MeasurementApi.getToolConfiguration(toolType);
|
|
||||||
const params = {
|
|
||||||
eventData,
|
|
||||||
tool,
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!tool) return;
|
|
||||||
|
|
||||||
if (tool.parentTool) {
|
|
||||||
handleChildMeasurementAdded(params);
|
|
||||||
} else {
|
|
||||||
handleSingleMeasurementAdded(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onModified(event) {
|
|
||||||
const eventData = getEventData(event);
|
|
||||||
const { toolType } = eventData;
|
|
||||||
const {
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
tool,
|
|
||||||
} = MeasurementApi.getToolConfiguration(toolType);
|
|
||||||
const params = {
|
|
||||||
eventData,
|
|
||||||
tool,
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!tool) return;
|
|
||||||
|
|
||||||
if (tool.parentTool) {
|
|
||||||
handleChildMeasurementModified(params);
|
|
||||||
} else {
|
|
||||||
handleSingleMeasurementModified(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemoved(event) {
|
|
||||||
const eventData = getEventData(event);
|
|
||||||
const { toolType } = eventData;
|
|
||||||
const {
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
tool,
|
|
||||||
} = MeasurementApi.getToolConfiguration(toolType);
|
|
||||||
const params = {
|
|
||||||
eventData,
|
|
||||||
tool,
|
|
||||||
toolGroupId,
|
|
||||||
toolGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!tool) return;
|
|
||||||
|
|
||||||
if (tool.parentTool) {
|
|
||||||
handleChildMeasurementRemoved(params);
|
|
||||||
} else {
|
|
||||||
handleSingleMeasurementRemoved(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MeasurementHandlers;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import * as tools from '../tools';
|
|
||||||
|
|
||||||
const childTools = [];
|
|
||||||
Object.keys(tools).forEach(key => childTools.push(tools[key]));
|
|
||||||
|
|
||||||
export const allTools = {
|
|
||||||
id: 'allTools',
|
|
||||||
name: 'Measurements',
|
|
||||||
childTools: childTools,
|
|
||||||
options: {
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { nonTarget } from '../tools';
|
|
||||||
|
|
||||||
export const nonTargets = {
|
|
||||||
id: 'nonTargets',
|
|
||||||
name: 'Non-Targets',
|
|
||||||
childTools: [nonTarget],
|
|
||||||
options: {
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { bidirectional, targetCR, targetUN, targetNE } from '../tools';
|
|
||||||
|
|
||||||
export const targets = {
|
|
||||||
id: 'targets',
|
|
||||||
name: 'Targets',
|
|
||||||
childTools: [bidirectional, targetCR, targetUN, targetNE],
|
|
||||||
options: {
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { length, ellipticalRoi } from '../tools';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
const childTools = cloneDeep([length, ellipticalRoi]);
|
|
||||||
|
|
||||||
// Exclude temp tools from case progress
|
|
||||||
childTools.forEach(childTool => {
|
|
||||||
childTool.options = Object.assign({}, childTool.options, {
|
|
||||||
caseProgress: {
|
|
||||||
include: false,
|
|
||||||
evaluate: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const temp = {
|
|
||||||
id: 'temp',
|
|
||||||
name: 'Temporary',
|
|
||||||
childTools,
|
|
||||||
options: {
|
|
||||||
caseProgress: {
|
|
||||||
include: false,
|
|
||||||
evaluate: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
let text = '';
|
|
||||||
if (data.rAngle && !isNaN(data.rAngle)) {
|
|
||||||
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const angle = {
|
|
||||||
id: 'Angle',
|
|
||||||
name: 'Angle',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'Angle',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
return data.text || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const arrowAnnotate = {
|
|
||||||
id: 'ArrowAnnotate',
|
|
||||||
name: 'ArrowAnnotate',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'ArrowAnnotate',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
if (data.shortestDiameter) {
|
|
||||||
// TODO: Make this check criteria again to see if we should display shortest x longest
|
|
||||||
return data.longestDiameter + ' x ' + data.shortestDiameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.longestDiameter;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bidirectional = {
|
|
||||||
id: 'Bidirectional',
|
|
||||||
name: 'Target',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'Bidirectional',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
let meanValue = '';
|
|
||||||
const { cachedStats } = data;
|
|
||||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
|
||||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
|
||||||
}
|
|
||||||
return meanValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const circleRoi = {
|
|
||||||
id: 'CircleRoi',
|
|
||||||
name: 'Circle',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'CircleRoi',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
let meanValue = '';
|
|
||||||
const { cachedStats } = data;
|
|
||||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
|
||||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
|
||||||
}
|
|
||||||
return meanValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ellipticalRoi = {
|
|
||||||
id: 'EllipticalRoi',
|
|
||||||
name: 'Ellipse',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'EllipticalRoi',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
const displayFunction = data => {
|
|
||||||
let meanValue = '';
|
|
||||||
if (data.meanStdDev && data.meanStdDev.mean && !isNaN(data.meanStdDev.mean)) {
|
|
||||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
|
||||||
}
|
|
||||||
return meanValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const freehandMouse = {
|
|
||||||
id: 'FreehandMouse',
|
|
||||||
name: 'Freehand',
|
|
||||||
toolGroup: 'allTools',
|
|
||||||
cornerstoneToolType: 'FreehandMouse',
|
|
||||||
options: {
|
|
||||||
measurementTable: {
|
|
||||||
displayFunction,
|
|
||||||
},
|
|
||||||
caseProgress: {
|
|
||||||
include: true,
|
|
||||||
evaluate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { arrowAnnotate } from './arrowAnnotate';
|
|
||||||
import { bidirectional } from './bidirectional';
|
|
||||||
import { ellipticalRoi } from './ellipticalRoi';
|
|
||||||
import { circleRoi } from './circleRoi';
|
|
||||||
import { freehandMouse } from './freehandMouse';
|
|
||||||
import { length } from './length';
|
|
||||||
import { nonTarget } from './nonTarget';
|
|
||||||
import { rectangleRoi } from './rectangleRoi';
|
|
||||||
import { angle } from './angle';
|
|
||||||
import { targetCR } from './targetCR';
|
|
||||||
import { targetNE } from './targetNE';
|
|
||||||
import { targetUN } from './targetUN';
|
|
||||||
|
|
||||||
export {
|
|
||||||
arrowAnnotate,
|
|
||||||
bidirectional,
|
|
||||||
ellipticalRoi,
|
|
||||||
circleRoi,
|
|
||||||
freehandMouse,
|
|
||||||
length,
|
|
||||||
nonTarget,
|
|
||||||
rectangleRoi,
|
|
||||||
angle,
|
|
||||||
targetCR,
|
|
||||||
targetNE,
|
|
||||||
targetUN,
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user