chore: Tag everything currently unused as legacy.

This commit is contained in:
James A. Petts 2020-07-15 15:43:28 +01:00 committed by Erik Ziegler
parent 8392c1f97e
commit 414ebc6850
161 changed files with 128 additions and 14712 deletions

View File

@ -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: [],

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &copy; 2016 nucleushealth&trade;. All rights reserved_

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
},
];

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
// Sorts an array by score
const sortByScore = arr => {
arr.sort((a, b) => {
return b.score - a.score;
});
};
export { sortByScore };

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
import ProtocolStore from './ProtocolStore';
import ProtocolStrategy from './ProtocolStrategy';
export { ProtocolStore, ProtocolStrategy };

View File

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

View File

@ -1,5 +0,0 @@
import { ProtocolStore, ProtocolStrategy } from './classes';
import defaultProtocol from './defaultProtocol';
import testProtocols from './testProtocols';
export { ProtocolStore, ProtocolStrategy, defaultProtocol, testProtocols };

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
import './cornerstone.js';
import './parsingUtils.js';

View File

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

View File

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

View File

@ -1,4 +0,0 @@
import MeasurementApi from './MeasurementApi';
import TimepointApi from './TimepointApi';
export { TimepointApi, MeasurementApi };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
import * as recistEvaluation from './recist.json';
export const recist11 = recistEvaluation;

View File

@ -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": {}
}

View File

@ -1,3 +0,0 @@
import ConformanceCriteria from './ConformanceCriteria';
export { ConformanceCriteria };

View File

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

View File

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

View File

@ -1,3 +0,0 @@
export default function(measurement) {
return measurement.description;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
import { nonTarget } from '../tools';
export const nonTargets = {
id: 'nonTargets',
name: 'Non-Targets',
childTools: [nonTarget],
options: {
caseProgress: {
include: true,
evaluate: true,
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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