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 OHIF from '@ohif/core';
|
||||
|
||||
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
||||
//import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
||||
import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const { ViewportGridService } = servicesManager.services;
|
||||
@ -299,9 +298,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
setCornerstoneLayout: () => {
|
||||
setCornerstoneLayout();
|
||||
},
|
||||
// setCornerstoneLayout: () => {
|
||||
// setCornerstoneLayout();
|
||||
// },
|
||||
setWindowLevel: ({ window, level }) => {
|
||||
const enabledElement = _getActiveViewportsEnabledElement();
|
||||
|
||||
@ -430,12 +429,12 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: { toolName: 'Zoom' },
|
||||
},
|
||||
setCornerstoneLayout: {
|
||||
commandFn: actions.setCornerstoneLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
// setCornerstoneLayout: {
|
||||
// commandFn: actions.setCornerstoneLayout,
|
||||
// storeContexts: [],
|
||||
// options: {},
|
||||
// context: 'VIEWER',
|
||||
// },
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: [],
|
||||
|
||||
@ -271,7 +271,7 @@ export default function init({
|
||||
}
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
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 { studies } from '@ohif/core';
|
||||
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
const { sortStudySeries, sortingCriteria } = studies;
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import log from '../log';
|
||||
import studies from '../studies';
|
||||
// import studies from '../studies';
|
||||
import utils from '../utils';
|
||||
import {
|
||||
retrieveMeasurementFromSR,
|
||||
stowSRFromMeasurements,
|
||||
} from './handleStructuredReport';
|
||||
// import {
|
||||
// retrieveMeasurementFromSR,
|
||||
// stowSRFromMeasurements,
|
||||
// } from './handleStructuredReport';
|
||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
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 { DICOMFileLoadingListener } from './StudyLoadingListener';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import ImageSet from './ImageSet';
|
||||
import MetadataProvider from './MetadataProvider';
|
||||
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 {
|
||||
OHIFStudyMetadataSource,
|
||||
MetadataProvider,
|
||||
CommandsManager,
|
||||
HotkeysManager,
|
||||
ImageSet,
|
||||
StudyPrefetcher,
|
||||
StudyLoadingListener,
|
||||
StackLoadingListener,
|
||||
DICOMFileLoadingListener,
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
};
|
||||
|
||||
const classes = {
|
||||
OHIFStudyMetadataSource,
|
||||
MetadataProvider,
|
||||
CommandsManager,
|
||||
HotkeysManager,
|
||||
ImageSet,
|
||||
StudyPrefetcher,
|
||||
StudyLoadingListener,
|
||||
StackLoadingListener,
|
||||
DICOMFileLoadingListener,
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
};
|
||||
|
||||
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 { ServicesManager } from './services';
|
||||
import classes, { CommandsManager, HotkeysManager } from './classes/';
|
||||
|
||||
import DICOMWeb from './DICOMWeb';
|
||||
import DICOMSR from './DICOMSR';
|
||||
import cornerstone from './cornerstone.js';
|
||||
import errorHandler from './errorHandler.js';
|
||||
import hangingProtocols from './hanging-protocols';
|
||||
import header from './header.js';
|
||||
import log from './log.js';
|
||||
import measurements from './measurements';
|
||||
import metadata from './classes/metadata/';
|
||||
import object from './object.js';
|
||||
import redux from './redux/';
|
||||
import string from './string.js';
|
||||
import studies from './studies/';
|
||||
import ui from './ui';
|
||||
import user from './user.js';
|
||||
import { ViewModelProvider, useViewModel } from './ViewModelContext';
|
||||
import utils from './utils/';
|
||||
@ -42,7 +32,7 @@ import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||
|
||||
const hotkeys = {
|
||||
...utils.hotkeys,
|
||||
defaults: { hotkeyBindings: defaults.hotkeyBindings }
|
||||
defaults: { hotkeyBindings: defaults.hotkeyBindings },
|
||||
};
|
||||
|
||||
const OHIF = {
|
||||
@ -56,14 +46,8 @@ const OHIF = {
|
||||
defaults,
|
||||
utils,
|
||||
hotkeys,
|
||||
studies,
|
||||
redux,
|
||||
classes,
|
||||
metadata,
|
||||
header,
|
||||
cornerstone,
|
||||
string,
|
||||
ui,
|
||||
user,
|
||||
errorHandler,
|
||||
object,
|
||||
@ -71,8 +55,6 @@ const OHIF = {
|
||||
DICOMWeb,
|
||||
DICOMSR,
|
||||
viewer: {},
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
CineService,
|
||||
UIDialogService,
|
||||
@ -102,22 +84,14 @@ export {
|
||||
defaults,
|
||||
utils,
|
||||
hotkeys,
|
||||
studies,
|
||||
redux,
|
||||
classes,
|
||||
metadata,
|
||||
header,
|
||||
cornerstone,
|
||||
string,
|
||||
ui,
|
||||
user,
|
||||
errorHandler,
|
||||
object,
|
||||
log,
|
||||
DICOMWeb,
|
||||
DICOMSR,
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
CineService,
|
||||
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