diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index 4bacc0d233a..459405cc963 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -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: [], diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 8e1003ea9be..8263b374b2d 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -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); diff --git a/extensions/default/src/DicomWebDataSource/utils/isLowPriorityModality.js b/extensions/default/src/DicomWebDataSource/utils/isLowPriorityModality.js new file mode 100644 index 00000000000..54024d5a051 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/isLowPriorityModality.js @@ -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); +} diff --git a/extensions/default/src/DicomWebDataSource/utils/sortStudy.js b/extensions/default/src/DicomWebDataSource/utils/sortStudy.js new file mode 100644 index 00000000000..e2af19bf886 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/sortStudy.js @@ -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 }; diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js index 22ff600c894..0020f4bf632 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js @@ -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 diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index cce0f881b84..11db0998981 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -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'; diff --git a/platform/core/src/DICOMSR/handleStructuredReport.js b/platform/core/src/DICOMSR/handleStructuredReport.js deleted file mode 100644 index fcaa19890b4..00000000000 --- a/platform/core/src/DICOMSR/handleStructuredReport.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/DICOMSR/parseDicomStructuredReport.js b/platform/core/src/DICOMSR/parseDicomStructuredReport.js deleted file mode 100644 index 84ab2d57b5b..00000000000 --- a/platform/core/src/DICOMSR/parseDicomStructuredReport.js +++ /dev/null @@ -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; diff --git a/platform/core/src/DICOMSR/parseMeasurementsData.js b/platform/core/src/DICOMSR/parseMeasurementsData.js deleted file mode 100644 index 359b1e27e3a..00000000000 --- a/platform/core/src/DICOMSR/parseMeasurementsData.js +++ /dev/null @@ -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; diff --git a/platform/core/src/measurements/conformance/ConformanceCriteria.js b/platform/core/src/LEGACY_measurements/conformance/ConformanceCriteria.js similarity index 100% rename from platform/core/src/measurements/conformance/ConformanceCriteria.js rename to platform/core/src/LEGACY_measurements/conformance/ConformanceCriteria.js diff --git a/platform/core/src/studies/index.js b/platform/core/src/LEGACY_studies/index.js similarity index 100% rename from platform/core/src/studies/index.js rename to platform/core/src/LEGACY_studies/index.js diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js b/platform/core/src/LEGACY_studies/services/wado/retrieveMetadataLoaderAsync.js similarity index 100% rename from platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js rename to platform/core/src/LEGACY_studies/services/wado/retrieveMetadataLoaderAsync.js diff --git a/platform/core/src/studies/sortStudy.js b/platform/core/src/LEGACY_studies/sortStudy.js similarity index 100% rename from platform/core/src/studies/sortStudy.js rename to platform/core/src/LEGACY_studies/sortStudy.js diff --git a/platform/core/src/classes/OHIFStudyMetadataSource.js b/platform/core/src/classes/OHIFStudyMetadataSource.js deleted file mode 100644 index 4e81f4113b1..00000000000 --- a/platform/core/src/classes/OHIFStudyMetadataSource.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/classes/StudyLoadingListener.js b/platform/core/src/classes/StudyLoadingListener.js deleted file mode 100644 index c655250bcde..00000000000 --- a/platform/core/src/classes/StudyLoadingListener.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/classes/StudyMetadataSource.js b/platform/core/src/classes/StudyMetadataSource.js deleted file mode 100644 index af9b6ac6c79..00000000000 --- a/platform/core/src/classes/StudyMetadataSource.js +++ /dev/null @@ -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' - ); - } -} diff --git a/platform/core/src/classes/StudyPrefetcher.js b/platform/core/src/classes/StudyPrefetcher.js deleted file mode 100644 index 5f384ad4c56..00000000000 --- a/platform/core/src/classes/StudyPrefetcher.js +++ /dev/null @@ -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(); - }; -} diff --git a/platform/core/src/classes/TypeSafeCollection.js b/platform/core/src/classes/TypeSafeCollection.js deleted file mode 100644 index 29eea0fad8f..00000000000 --- a/platform/core/src/classes/TypeSafeCollection.js +++ /dev/null @@ -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'); - } -} diff --git a/platform/core/src/classes/index.js b/platform/core/src/classes/index.js index 9fc2f1bfee4..c23050b69e9 100644 --- a/platform/core/src/classes/index.js +++ b/platform/core/src/classes/index.js @@ -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; diff --git a/platform/core/src/classes/metadata/InstanceMetadata.js b/platform/core/src/classes/metadata/InstanceMetadata.js deleted file mode 100644 index 27c899c80ba..00000000000 --- a/platform/core/src/classes/metadata/InstanceMetadata.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/classes/metadata/Metadata.js b/platform/core/src/classes/metadata/Metadata.js deleted file mode 100644 index 3bf8dc8dfb2..00000000000 --- a/platform/core/src/classes/metadata/Metadata.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/classes/metadata/OHIFInstanceMetadata.js b/platform/core/src/classes/metadata/OHIFInstanceMetadata.js deleted file mode 100644 index 884016182ee..00000000000 --- a/platform/core/src/classes/metadata/OHIFInstanceMetadata.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/classes/metadata/OHIFSeriesMetadata.js b/platform/core/src/classes/metadata/OHIFSeriesMetadata.js deleted file mode 100644 index ba2c71ea309..00000000000 --- a/platform/core/src/classes/metadata/OHIFSeriesMetadata.js +++ /dev/null @@ -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)); - }); - } -} diff --git a/platform/core/src/classes/metadata/OHIFStudyMetadata.js b/platform/core/src/classes/metadata/OHIFStudyMetadata.js deleted file mode 100644 index 8ebf574ff5f..00000000000 --- a/platform/core/src/classes/metadata/OHIFStudyMetadata.js +++ /dev/null @@ -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)); - }); - } -} diff --git a/platform/core/src/classes/metadata/README.md b/platform/core/src/classes/metadata/README.md deleted file mode 100644 index 0ce89b17e44..00000000000 --- a/platform/core/src/classes/metadata/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# Study Metadata Module - -This module defines the API/Data-Model by which OHIF Viewerbase package and -possibly distinct viewer implementations can access studies metadata. This -module does not attempt to define any means of _loading_ study metadata from any -data end-point but only how the data that has been previously loaded into the -application context will be accessed by any of the routines or algorithm -implementations that need the data. - -## Intro - -For various reasons like sorting, grouping or simply rendering study -information, OHIF Viewerbase package and applications depending on it usually -have the need to access study metadata. Before the current initiative there was -no uniform way of achieving that since each implementation provides study -metadata on its own specific ways. The application and the package itself needed -to have a deep knowledge of the data structures provided by the data endpoint to -perform any of the operations mentioned above, meaning that any data access code -needed to be adapted or rewritten. - -The intent of the current module is to provide a fairly consistent and flexible -API/Data-Model by which OHIF Viewerbase package (and different viewer -implementations that depend on it) can manipulate DICOM matadata retrieved from -distinct data end points (e.g., a proprietary back end servers) in uniform ways -with minor to no modifications needed. - -## Implementation - -The current API implementation defines three classes of objects: -`StudyMetadata`, `SeriesMetadata` and `InstanceMetadata`. Inside OHIF Viewerbase -package, every access to Study, Series or SOP Instance metadata is achieved by -the interface exposed by these three classes. By inheriting from them and -overriding or extending their methods, different applications with different -data models can adapt even the most peculiar data structures to the uniform -interface defined by those classes. Together these classes define a flexible and -extensible data manipulation layer leaving routines and algorithms that depend -on that data untouched. - -## Design Decisions & "_Protected_" Members - -In order to provide for good programming practices, attributes and methods meant -to be used exclusively by the classes themselves (for internal purposes only) -were written with an initial '\_' character, being thus treated as "_protected_" -members. The idea behind this practice was never to hide them from the -programmers (what makes debugging tasks painful) but only advise for something -that's not part of the official public API and thus should not be relied on. -Usage of "protected" members makes the code less readable and prone to -compatibility issues. - -As an example, the initial implementation of the `StudyMetadata` class defined -the attribute `_studyInstanceUID` and the method `getStudyInstanceUID`. This -implies that whenever the _StudyInstanceUID_ of a given study needs to be -retrieved the `getStudyInstanceUID` method should be called instead of directly -accessing the attribute `_studyInstanceUID` (which might not even be populated -since `getStudyInstanceUID` can be possiblity overriden by a subclass to satisfy -specific implementation needs, leaving the attribute `_studyInstanceUID` -unused). - -Ex: - -```javascript -let studyUID = myStudy.getStudyInstanceUID(); // GOOD! :-) -[ ... ] -let otherStudyUID = anotherStudy._studyInstanceUID; // BAD... :-( -``` - -Another important topic is the preference of _methods_ over _attributes_ on the -public API. This design decision was made to ensure extensibility and -flexibility (methods are extensible while standalone attributes are not, and can -be adapted – through overrides, for example – to support even the most peculiar -data models) even though the overhead a few additional function calls may incur. - -## Abstract Classes - -Some classes defined in this module are "_abstract_" classes (even though -JavaScript does not _officially_ support such programming facility). They are -_abstract_ in the sense that a few methods (very important ones, by the way) -were left "_blank_" (unimplemented, or more precisely implemented as empty NOP -functions) in order to be implemented by specialized subclasses. Methods -believed to be more generic were implemented in an attempt to satify most -implementation needs but nothing prevents a subclass from overriding them as -well (again, flexibility and extensibility are design goals). Most implemented -methods rely on the implementation of an unimplemented method. For example, the -method `getStringValue` from `InstanceMetadata` class, which has indeed been -implemented and is meant to retrieve a metadata value as a string, internally -calls the `getTagValue` method which _was NOT implemented_ and is meant to query -the internal data structures for the requested metadata value and return it _as -is_. Used in that way, an application would not benefit much from the already -implemented methods. On the other hand, by simply overriding the `getTagValue` -method on a specialized class to deal with the intrinsics of its internal data -structures, this very application would now benefit from all already implemented -methods. - -The following code snippet tries to illustrate the idea: - -```javascript - -// -- InstanceMetadata.js - -class InstanceMetadata { - [ ... ] - getTagValue(tagOrProperty, defaultValue) { - // Please implement this method in a specialized subclass... - } - [ ... ] - getStringValue(tagOrProperty, index, defaultValue) { - let rawValue = this.getTagValue(tagOrProperty, ''); - // parse the returned value into a string... - [ ... ] - return stringValue; - } - [ ... ] -} - -// -- MyFancyAppInstanceMetadata.js - -class MyFancyAppInstanceMetadata extends InstanceMetadata { - // Overriding this method will make all methods implemented in the super class - // that rely on it to be immediately available... - getTagValue(tagOrProperty, defaultValue) { - let tagValue; - // retrieve raw value from internal data structures... - [ ... ] - return tagValue; - } -} - -// -- main.js - -[ ... ] -let sopInstaceMetadata = new MyFancyAppInstanceMetadata(myInternalData); -if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true - // this code will be executed... -} -if (sopInstaceMetadata instanceof InstanceMetadata) { // also true - // this code will also be executed... -} -// The following will also work since the internal "getTagValue" call inside -// "getStringValue" method will now be satisfied... (thanks to the override) -let PatientName = sopInstaceMetadata.getStringValue('PatientName', ''); -[ ... ] - -``` - -_Copyright © 2016 nucleushealth™. All rights reserved_ diff --git a/platform/core/src/classes/metadata/SeriesMetadata.js b/platform/core/src/classes/metadata/SeriesMetadata.js deleted file mode 100644 index 84ae4a037e9..00000000000 --- a/platform/core/src/classes/metadata/SeriesMetadata.js +++ /dev/null @@ -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()) - ); - } -} diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js deleted file mode 100644 index 911427f6778..00000000000 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ /dev/null @@ -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); -} diff --git a/platform/core/src/classes/metadata/index.js b/platform/core/src/classes/metadata/index.js deleted file mode 100644 index 227654142b2..00000000000 --- a/platform/core/src/classes/metadata/index.js +++ /dev/null @@ -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; diff --git a/platform/core/src/cornerstone.js b/platform/core/src/cornerstone.js deleted file mode 100644 index 7940792ffbd..00000000000 --- a/platform/core/src/cornerstone.js +++ /dev/null @@ -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; diff --git a/platform/core/src/hanging-protocols/HPMatcher.js b/platform/core/src/hanging-protocols/HPMatcher.js deleted file mode 100644 index 26eea7ef339..00000000000 --- a/platform/core/src/hanging-protocols/HPMatcher.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/ProtocolEngine.js b/platform/core/src/hanging-protocols/ProtocolEngine.js deleted file mode 100644 index 492d71dfd24..00000000000 --- a/platform/core/src/hanging-protocols/ProtocolEngine.js +++ /dev/null @@ -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'); - } - } -} diff --git a/platform/core/src/hanging-protocols/classes/Protocol.js b/platform/core/src/hanging-protocols/classes/Protocol.js deleted file mode 100644 index 5e71825aeb7..00000000000 --- a/platform/core/src/hanging-protocols/classes/Protocol.js +++ /dev/null @@ -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(); - } - } -} diff --git a/platform/core/src/hanging-protocols/classes/Rule.js b/platform/core/src/hanging-protocols/classes/Rule.js deleted file mode 100644 index 29f93c7910b..00000000000 --- a/platform/core/src/hanging-protocols/classes/Rule.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/hanging-protocols/classes/Stage.js b/platform/core/src/hanging-protocols/classes/Stage.js deleted file mode 100644 index f6cb34ae91e..00000000000 --- a/platform/core/src/hanging-protocols/classes/Stage.js +++ /dev/null @@ -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); - }); - } - } -} diff --git a/platform/core/src/hanging-protocols/classes/Viewport.js b/platform/core/src/hanging-protocols/classes/Viewport.js deleted file mode 100644 index 6187c3298d5..00000000000 --- a/platform/core/src/hanging-protocols/classes/Viewport.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/hanging-protocols/classes/ViewportStructure.js b/platform/core/src/hanging-protocols/classes/ViewportStructure.js deleted file mode 100644 index 908ef296236..00000000000 --- a/platform/core/src/hanging-protocols/classes/ViewportStructure.js +++ /dev/null @@ -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; - } - } -} diff --git a/platform/core/src/hanging-protocols/classes/index.js b/platform/core/src/hanging-protocols/classes/index.js deleted file mode 100644 index b27b088de77..00000000000 --- a/platform/core/src/hanging-protocols/classes/index.js +++ /dev/null @@ -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, -}; diff --git a/platform/core/src/hanging-protocols/classes/rules.js b/platform/core/src/hanging-protocols/classes/rules.js deleted file mode 100644 index 3c94b71c5b6..00000000000 --- a/platform/core/src/hanging-protocols/classes/rules.js +++ /dev/null @@ -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, -}; diff --git a/platform/core/src/hanging-protocols/customAttributes.js b/platform/core/src/hanging-protocols/customAttributes.js deleted file mode 100644 index 21a481ef24e..00000000000 --- a/platform/core/src/hanging-protocols/customAttributes.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/customViewportSettings.js b/platform/core/src/hanging-protocols/customViewportSettings.js deleted file mode 100644 index c47f1ce3b13..00000000000 --- a/platform/core/src/hanging-protocols/customViewportSettings.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/hardcodedData.js b/platform/core/src/hanging-protocols/hardcodedData.js deleted file mode 100644 index 47781f994da..00000000000 --- a/platform/core/src/hanging-protocols/hardcodedData.js +++ /dev/null @@ -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', - }, -]; diff --git a/platform/core/src/hanging-protocols/index.js b/platform/core/src/hanging-protocols/index.js deleted file mode 100644 index b0d2cdb312f..00000000000 --- a/platform/core/src/hanging-protocols/index.js +++ /dev/null @@ -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; diff --git a/platform/core/src/hanging-protocols/lib/comparators.js b/platform/core/src/hanging-protocols/lib/comparators.js deleted file mode 100644 index ce76a4d822a..00000000000 --- a/platform/core/src/hanging-protocols/lib/comparators.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/lib/displayConstraint.js b/platform/core/src/hanging-protocols/lib/displayConstraint.js deleted file mode 100644 index 17368c87721..00000000000 --- a/platform/core/src/hanging-protocols/lib/displayConstraint.js +++ /dev/null @@ -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; -} diff --git a/platform/core/src/hanging-protocols/lib/removeFromArray.js b/platform/core/src/hanging-protocols/lib/removeFromArray.js deleted file mode 100644 index 602cc415850..00000000000 --- a/platform/core/src/hanging-protocols/lib/removeFromArray.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/lib/sortByScore.js b/platform/core/src/hanging-protocols/lib/sortByScore.js deleted file mode 100644 index 902072591bf..00000000000 --- a/platform/core/src/hanging-protocols/lib/sortByScore.js +++ /dev/null @@ -1,8 +0,0 @@ -// Sorts an array by score -const sortByScore = arr => { - arr.sort((a, b) => { - return b.score - a.score; - }); -}; - -export { sortByScore }; diff --git a/platform/core/src/hanging-protocols/lib/validate.js b/platform/core/src/hanging-protocols/lib/validate.js deleted file mode 100644 index deb9521c0f3..00000000000 --- a/platform/core/src/hanging-protocols/lib/validate.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStore.js b/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStore.js deleted file mode 100644 index 956347b2f05..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStore.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStrategy.js b/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStrategy.js deleted file mode 100644 index 46079287298..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/classes/ProtocolStrategy.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/hanging-protocols/protocolStore/classes/index.js b/platform/core/src/hanging-protocols/protocolStore/classes/index.js deleted file mode 100644 index 3a9fd4cfcdb..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/classes/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import ProtocolStore from './ProtocolStore'; -import ProtocolStrategy from './ProtocolStrategy'; - -export { ProtocolStore, ProtocolStrategy }; diff --git a/platform/core/src/hanging-protocols/protocolStore/defaultProtocol.js b/platform/core/src/hanging-protocols/protocolStore/defaultProtocol.js deleted file mode 100644 index 52beaeb9244..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/defaultProtocol.js +++ /dev/null @@ -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; diff --git a/platform/core/src/hanging-protocols/protocolStore/index.js b/platform/core/src/hanging-protocols/protocolStore/index.js deleted file mode 100644 index 5bb4ba7da67..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { ProtocolStore, ProtocolStrategy } from './classes'; -import defaultProtocol from './defaultProtocol'; -import testProtocols from './testProtocols'; - -export { ProtocolStore, ProtocolStrategy, defaultProtocol, testProtocols }; diff --git a/platform/core/src/hanging-protocols/protocolStore/testProtocols.js b/platform/core/src/hanging-protocols/protocolStore/testProtocols.js deleted file mode 100644 index 23f5b853934..00000000000 --- a/platform/core/src/hanging-protocols/protocolStore/testProtocols.js +++ /dev/null @@ -1,1510 +0,0 @@ -import Protocol from '../classes/Protocol'; -import ViewportStructure from '../classes/ViewportStructure'; -import Viewport from '../classes/Viewport'; -import Stage from '../classes/Stage'; -import { - ImageMatchingRule, - ProtocolMatchingRule, - SeriesMatchingRule, -} from '../classes'; - -function getMRTwoByTwoTest() { - var proto = new Protocol('MR_TwoByTwo'); - proto.id = 'MR_TwoByTwo'; - proto.locked = true; - // Use http://localhost:3000/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78 - - var StudyInstanceUID = new ProtocolMatchingRule( - 'StudyInstanceUID', - { - equals: { - value: '1.2.840.113619.2.5.1762583153.215519.978957063.78', - }, - }, - true - ); - - proto.addProtocolMatchingRule(StudyInstanceUID); - - var oneByTwo = new ViewportStructure('grid', { - Rows: 1, - Columns: 2, - }); - - // Stage 1 - var left = new Viewport(); - var right = new Viewport(); - - var firstSeries = new SeriesMatchingRule('SeriesNumber', { - equals: { - value: 1, - }, - }); - - var secondSeries = new SeriesMatchingRule('SeriesNumber', { - equals: { - value: 2, - }, - }); - - var thirdImage = new ImageMatchingRule('InstanceNumber', { - equals: { - value: 3, - }, - }); - - left.seriesMatchingRules.push(firstSeries); - left.imageMatchingRules.push(thirdImage); - - right.seriesMatchingRules.push(secondSeries); - right.imageMatchingRules.push(thirdImage); - - var first = new Stage(oneByTwo, 'oneByTwo'); - first.viewports.push(left); - first.viewports.push(right); - - proto.stages.push(first); - - // Stage 2 - var twoByOne = new ViewportStructure('grid', { - Rows: 2, - Columns: 1, - }); - var left2 = new Viewport(); - var right2 = new Viewport(); - - var fourthSeries = new SeriesMatchingRule('SeriesNumber', { - equals: { - value: 4, - }, - }); - - var fifthSeries = new SeriesMatchingRule('SeriesNumber', { - equals: { - value: 5, - }, - }); - - left2.seriesMatchingRules.push(fourthSeries); - left2.imageMatchingRules.push(thirdImage); - right2.seriesMatchingRules.push(fifthSeries); - right2.imageMatchingRules.push(thirdImage); - - var second = new Stage(twoByOne, 'twoByOne'); - second.viewports.push(left2); - second.viewports.push(right2); - - proto.stages.push(second); - - return proto; -} - -function getDemoProtocols() { - const demoProtocols = []; - - /** - * Demo #1 - */ - demoProtocols.push({ - id: 'demoProtocol1', - locked: false, - name: 'DFCI-CT-CHEST-COMPARE', - createdDate: '2017-02-14T16:07:09.033Z', - modifiedDate: '2017-02-14T16:18:43.930Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: '7tmuq7KzDMCWFeapc', - weight: 2, - required: false, - attribute: 'x00081030', - constraint: { - contains: { - value: 'DFCI CT CHEST', - }, - }, - }, - ], - stages: [ - { - id: 'v5PfGt9F6mffZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '2.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '2.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'XTzu8HB3feep3HYKs', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '3.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '3.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:12.085Z', - }, - { - id: '3yPYNaeFtr76Qz3jq', - viewportStructure: { - type: 'grid', - properties: { - Rows: 2, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: { - wlPreset: 'Lung', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 3.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: '6vdBRZYnqmmosipph', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'SxfTyhGcMhr56PtPM', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - { - viewportSettings: { - wlPreset: 'Lung', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'FTAyChZCPW68yJjXD', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 3.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'gMJjfrbsqYNbErPx5', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:11:40.489Z', - }, - ], - numberOfPriorsReferenced: 4, - }); - - /** - * Demo #2 - */ - - demoProtocols.push({ - id: 'demoProtocol2', - locked: false, - name: 'DFCI-CT-CHEST-COMPARE-2', - createdDate: '2017-02-14T16:07:09.033Z', - modifiedDate: '2017-02-14T16:18:43.930Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: '7tmuq7KzDMCWFeapc', - weight: 2, - required: false, - attribute: 'x00081030', - constraint: { - contains: { - value: 'DFCI CT CHEST', - }, - }, - }, - ], - stages: [ - { - id: 'v5PfGt9F6mffZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mac', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '2.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYc', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '2.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnXTByWnPt', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'XTzu8HB3feep3HYKs', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - { - id: 'mYnsCcNwZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 5.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - { - id: 'ygz4nb29iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 5.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:12.085Z', - }, - { - id: '3yPYNaeFtr76Qz3jq', - viewportStructure: { - type: 'grid', - properties: { - Rows: 2, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7mtr', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - { - id: 'jXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 5.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: { - wlPreset: 'Lung', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'ygz4nb28iJZcJhnYb', - weight: 2, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 3.0', - }, - }, - }, - { - id: 'ycz4nb28iJZcJhnYa', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 5.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: '6vdBRZYnqmmosipph', - weight: 2, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - { - id: '6vdBRFYnqmmosipph', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 5.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'SxfTyhGcMhr56PtPM', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - { - viewportSettings: { - wlPreset: 'Lung', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'FTAyChZCPW68yJjXD', - weight: 2, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 3.0', - }, - }, - }, - { - id: 'DTAyChZCPW68yJjXD', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 5.0', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'gMJjfrbsqYNbErPx5', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:11:40.489Z', - }, - ], - numberOfPriorsReferenced: 1, - }); - - /** - * Demo: screenCT - */ - - demoProtocols.push({ - id: 'screenCT', - locked: false, - name: 'DFCI-CT-CHEST-SCREEN', - createdDate: '2017-02-14T16:07:09.033Z', - modifiedDate: '2017-02-14T16:18:43.930Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: '7tmuq7KzDMCWFeapc', - weight: 2, - required: false, - attribute: 'x00081030', - constraint: { - contains: { - value: 'DFCI CT CHEST', - }, - }, - }, - ], - stages: [ - { - id: 'v5PfGt9F6mffZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 1, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL55z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: '2.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F4mffZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 2, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7nTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 5.0', - }, - }, - }, - { - id: 'mXnsCcNzZL56z7rTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 3.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56r7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 5.0', - }, - }, - }, - { - id: 'mXnsCcNzZL56a7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Lung 3.0', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcRzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 4.0', - }, - }, - }, - { - id: 'mXnsCcNzTL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Coronal', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcMzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Body 4.0', - }, - }, - }, - { - id: 'mXnsCcAzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Sagittal', - }, - }, - }, - ], - studyMatchingRules: [], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - ], - numberOfPriorsReferenced: 0, - }); - - /** - * Demo: PETCTSCREEN - */ - - demoProtocols.push({ - id: 'PETCTSCREEN', - locked: false, - name: 'PETCT-SCREEN', - createdDate: '2017-02-14T16:07:09.033Z', - modifiedDate: '2017-02-14T16:18:43.930Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: '7tmuqgKzDMCWFeapc', - weight: 5, - required: false, - attribute: 'x00081030', - constraint: { - contains: { - value: 'PETCT', - }, - }, - }, - ], - stages: [ - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcAzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZR56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - { - id: 'mRnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x00200011', - constraint: { - numericality: { - greaterThanOrEqualTo: 2, - }, - }, - }, - ], - studyMatchingRules: [], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsGcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Corrected', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsHcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT WB', - }, - }, - }, - ], - studyMatchingRules: [], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: { - invert: 'YES', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXneCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Uncorrected', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCuNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT Nk', - }, - }, - }, - ], - studyMatchingRules: [], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - ], - numberOfPriorsReferenced: 0, - }); - - /** - * Demo: PETCTCOMPARE - */ - - demoProtocols.push({ - id: 'PETCTCOMPARE', - locked: false, - name: 'PETCT-COMPARE', - createdDate: '2017-02-14T16:07:09.033Z', - modifiedDate: '2017-02-14T16:18:43.930Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: '7tmuqgKzDMCWFeapc', - weight: 5, - required: false, - attribute: 'x00081030', - constraint: { - contains: { - value: 'PETCT', - }, - }, - }, - ], - stages: [ - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL59z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7lTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTbnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 1, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNjZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - { - id: 'mXnsCcNzZL56z7gTZ', - weight: 1, - required: false, - attribute: 'x00200011', - constraint: { - numericality: { - greaterThanOrEqualTo: 2, - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcCzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'Topogram', - }, - }, - }, - { - id: 'mXnsCcNzZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x00200011', - constraint: { - numericality: { - greaterThanOrEqualTo: 2, - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvn1TByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 2, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL26z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Corrected', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL46z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT WB', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL57z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Corrected', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnYTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZQ56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT WB', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgLTvnKTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - { - id: 'v5PfGt9F6mFgZPif5', - name: 'oneByOne', - viewportStructure: { - type: 'grid', - properties: { - Rows: 2, - Columns: 2, - }, - layoutTemplateName: 'gridLayout', - }, - viewports: [ - { - viewportSettings: { - invert: 'YES', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZL56z7nTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Uncorrected', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNxZL56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT Nk', - }, - }, - }, - ], - studyMatchingRules: [], - }, - { - viewportSettings: { - invert: 'YES', - }, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZA56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'PET WB Uncorrected', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgHTvnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - { - viewportSettings: {}, - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'mXnsCcNzZP56z7mTZ', - weight: 1, - required: false, - attribute: 'x0008103e', - constraint: { - contains: { - value: 'CT Nk', - }, - }, - }, - ], - studyMatchingRules: [ - { - id: 'uDoEgITvnXTByWnPz', - weight: 1, - required: false, - attribute: 'abstractPriorValue', - constraint: { - equals: { - value: 1, - }, - }, - }, - ], - }, - ], - createdDate: '2017-02-14T16:07:09.033Z', - }, - ], - numberOfPriorsReferenced: 1, - }); - - return demoProtocols; -} - -const testProtocols = { - getMRTwoByTwoTest, - getDemoProtocols, -}; - -export default testProtocols; diff --git a/platform/core/src/header.js b/platform/core/src/header.js deleted file mode 100644 index 34e06773b63..00000000000 --- a/platform/core/src/header.js +++ /dev/null @@ -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; diff --git a/platform/core/src/index.js b/platform/core/src/index.js index 9eb735a10ec..c84bcd121a7 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -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, diff --git a/platform/core/src/lib/cornerstone.js b/platform/core/src/lib/cornerstone.js deleted file mode 100644 index 4a321addf1f..00000000000 --- a/platform/core/src/lib/cornerstone.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/lib/index.js b/platform/core/src/lib/index.js deleted file mode 100644 index dee5f06e9ec..00000000000 --- a/platform/core/src/lib/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './cornerstone.js'; -import './parsingUtils.js'; diff --git a/platform/core/src/lib/parsingUtils.js b/platform/core/src/lib/parsingUtils.js deleted file mode 100644 index 0d78a0665e9..00000000000 --- a/platform/core/src/lib/parsingUtils.js +++ /dev/null @@ -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); - }, -}; diff --git a/platform/core/src/measurements/classes/MeasurementApi.js b/platform/core/src/measurements/classes/MeasurementApi.js deleted file mode 100644 index c8a7eed87eb..00000000000 --- a/platform/core/src/measurements/classes/MeasurementApi.js +++ /dev/null @@ -1,1027 +0,0 @@ -import cornerstoneTools from 'cornerstone-tools'; -import cornerstone from 'cornerstone-core'; -import log from '../../log'; -import getLabel from '../lib/getLabel'; -import getDescription from '../lib/getDescription'; -import getImageIdForImagePath from '../lib/getImageIdForImagePath'; -import guid from '../../utils/guid'; -import studyMetadataManager from '../../utils/studyMetadataManager'; -import { measurementApiDefaultConfig } from './../configuration.js'; - -const configuration = { - ...measurementApiDefaultConfig, -}; - -export default class MeasurementApi { - static Instance; - - /** - * Set configuration: It should merge default configuration with any new one - * - * @static - * @param {Object} config - * @param {Object} config.server - * @param {string} config.server.type - The server type - * @param {string} config.server.wadoRoot - The server wado URL root - * @param {Array} config.measurementTools - * @param {string} config.measurementTools[].id - The tool group id - * @param {string} config.measurementTools[].name - The tool group name - * @param {Array} config.measurementTools[].childTools - The child tool's configuration - * @param {Object} config.dataExchange - * @param {Function} config.dataExchange.store - Function that store measurement data - * @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data - * - * @memberof MeasurementApi - */ - static setConfiguration(config) { - Object.assign(configuration, config); - } - - static getConfiguration() { - return configuration; - } - - static getToolsGroupsMap() { - const toolsGroupsMap = {}; - configuration.measurementTools.forEach(toolGroup => { - toolGroup.childTools.forEach( - tool => (toolsGroupsMap[tool.id] = toolGroup.id) - ); - }); - - return toolsGroupsMap; - } - - static getToolGroupTools(toolsGroupsMap) { - const result = {}; - Object.keys(toolsGroupsMap).forEach(toolType => { - const toolGroupId = toolsGroupsMap[toolType]; - if (!result[toolGroupId]) { - result[toolGroupId] = []; - } - - result[toolGroupId].push(toolType); - }); - - return result; - } - - static getToolConfiguration(toolType) { - const configuration = MeasurementApi.getConfiguration(); - const toolsGroupsMap = MeasurementApi.getToolsGroupsMap(); - - const toolGroupId = toolsGroupsMap[toolType]; - const toolGroup = configuration.measurementTools.find( - toolGroup => toolGroup.id === toolGroupId - ); - - let tool; - if (toolGroup) { - tool = toolGroup.childTools.find(tool => tool.id === toolType); - } - - return { - toolGroupId, - toolGroup, - tool, - }; - } - - static syncMeasurementAndToolData(measurement) { - log.info('syncMeasurementAndToolData'); - - const measurementLabel = getLabel(measurement); - if (measurementLabel) { - measurement.labels = [measurementLabel]; - } - - const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState(); - - // Stop here if the metadata for the measurement's study is not loaded yet - const { StudyInstanceUID } = measurement; - const metadata = studyMetadataManager.get(StudyInstanceUID); - if (!metadata) return; - - // Iterate each child tool if the current tool has children - const toolType = measurement.toolType; - const { tool } = MeasurementApi.getToolConfiguration(toolType); - if (Array.isArray(tool.childTools)) { - tool.childTools.forEach(childToolKey => { - const childMeasurement = measurement[childToolKey]; - if (!childMeasurement) return; - childMeasurement._id = measurement._id; - childMeasurement.measurementNumber = measurement.measurementNumber; - childMeasurement.lesionNamingNumber = measurement.lesionNamingNumber; - - MeasurementApi.syncMeasurementAndToolData(childMeasurement); - }); - - return; - } - - const imageId = getImageIdForImagePath(measurement.imagePath); - - // If no tool state exists for this imageId, create an empty object to store it - if (!toolState[imageId]) { - toolState[imageId] = {}; - } - - const currentToolState = toolState[imageId][toolType]; - const toolData = currentToolState && currentToolState.data; - - // Check if we already have toolData for this imageId and toolType - if (toolData && toolData.length) { - // If we have toolData, we should search it for any data related to the current Measurement - const toolData = toolState[imageId][toolType].data; - - // Create a flag so we know if we've successfully updated the Measurement in the toolData - let alreadyExists = false; - - // Loop through the toolData to search for this Measurement - toolData.forEach(tool => { - // Break the loop if this isn't the Measurement we are looking for - if (tool._id !== measurement._id) { - return; - } - - // If we have found the Measurement, set the flag to True - alreadyExists = true; - - // Update the toolData from the Measurement data - Object.assign(tool, measurement); - return false; - }); - - // If we have found the Measurement we intended to update, we can stop this function here - if (alreadyExists === true) { - return; - } - } else { - // If no toolData exists for this toolType, create an empty array to hold some - toolState[imageId][toolType] = { - data: [], - }; - } - - // If we have reached this point, it means we haven't found the Measurement we are looking for - // in the current toolData. This means we need to add it. - - // Add the MeasurementData into the toolData for this imageId - toolState[imageId][toolType].data.push(measurement); - - cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState( - toolState - ); - } - - static isToolIncluded(tool) { - return ( - tool.options && - tool.options.caseProgress && - tool.options.caseProgress.include - ); - } - - constructor(timepointApi, options = {}) { - if (MeasurementApi.Instance) { - MeasurementApi.Instance.initialize(timepointApi, options); - return MeasurementApi.Instance; - } - - this.initialize(timepointApi, options); - MeasurementApi.Instance = this; - } - - initialize(timepointApi, options = {}) { - this.timepointApi = timepointApi; - this.options = options; - this.toolGroups = {}; - this.tools = {}; - this.toolsGroupsMap = MeasurementApi.getToolsGroupsMap(); - this.toolGroupTools = MeasurementApi.getToolGroupTools(this.toolsGroupsMap); - - // Iterate over each tool group and create collection - configuration.measurementTools.forEach(toolGroup => { - this.toolGroups[toolGroup.id] = []; - - // Iterate over each tool group child tools (e.g. bidirectional, targetCR, etc.) and create collection - toolGroup.childTools.forEach(tool => { - this.tools[tool.id] = []; - }); - }); - } - - onMeasurementsUpdated() { - if (typeof this.options.onMeasurementsUpdated !== 'function') { - log.warn('Measurements update callback is not defined'); - return; - } - - this.options.onMeasurementsUpdated(Object.assign({}, this.tools)); - } - - retrieveMeasurements(PatientID, timepointIds) { - const retrievalFn = configuration.dataExchange.retrieve; - const { server } = configuration; - if (typeof retrievalFn !== 'function') { - log.error('Measurement retrieval function has not been configured.'); - return; - } - - return new Promise((resolve, reject) => { - retrievalFn(server).then(measurementData => { - if (measurementData) { - log.info('Measurement data retrieval'); - log.info(measurementData); - - Object.keys(measurementData).forEach(measurementTypeId => { - const measurements = measurementData[measurementTypeId]; - - measurements.forEach(measurement => { - const { toolType } = measurement; - - this.addMeasurement(toolType, measurement); - }); - }); - } - - resolve(); - - // Synchronize the new tool data - this.syncMeasurementsAndToolData(); - - cornerstone.getEnabledElements().forEach(enabledElement => { - cornerstone.updateImage(enabledElement.element); - }); - - // Let others know that the measurements are updated - this.onMeasurementsUpdated(); - }, reject); - }); - } - - storeMeasurements(timepointId) { - const { server } = configuration; - const storeFn = configuration.dataExchange.store; - if (typeof storeFn !== 'function') { - log.error('Measurement store function has not been configured.'); - return; - } - - let measurementData = {}; - configuration.measurementTools.forEach(toolGroup => { - // Skip the tool groups excluded from case progress - if (!MeasurementApi.isToolIncluded(toolGroup)) { - return; - } - - toolGroup.childTools.forEach(tool => { - // Skip the tools excluded from case progress - if (!MeasurementApi.isToolIncluded(tool)) { - return; - } - - if (!measurementData[toolGroup.id]) { - measurementData[toolGroup.id] = []; - } - - measurementData[toolGroup.id] = measurementData[toolGroup.id].concat( - this.tools[tool.id] - ); - }); - }); - - const timepointFilter = timepointId - ? tp => tp.timepointId === timepointId - : null; - const timepoints = this.timepointApi.all(timepointFilter); - const timepointIds = timepoints.map(t => t.timepointId); - const PatientID = timepoints[0].PatientID; - const filter = { - PatientID, - timepointIds, - }; - - log.info('Saving Measurements for timepoints:', timepoints); - return storeFn(measurementData, filter, server).then(result => { - log.info('Measurement storage completed'); - return result; - }); - } - - calculateLesionNamingNumber(measurements) { - const sortedMeasurements = measurements.sort((a, b) => { - if (a.lesionNamingNumber > b.lesionNamingNumber) { - return 1; - } else if (a.lesionNamingNumber < b.lesionNamingNumber) { - return -1; - } - - return 0; - }); - - // Calculate lesion naming number starting from 1 not to miss any measurement (as seen in MM) - // A measurement from beginning of the list might be deleted, so a new measurement should replace that - let i; - for (i = 1; i < sortedMeasurements.length + 1; i++) { - if (i < sortedMeasurements[i - 1].lesionNamingNumber) { - break; - } - } - - return i; - } - - fetch(toolGroupId, filter) { - if (!this.toolGroups[toolGroupId]) { - throw new Error( - `MeasurementApi: No Collection with the id: ${toolGroupId}` - ); - } - - let items; - if (filter) { - items = this.toolGroups[toolGroupId].filter(filter); - } else { - items = this.toolGroups[toolGroupId]; - } - - return items.map(item => { - if (item.toolId) { - return this.tools[item.toolId].find( - tool => tool._id === item.toolItemId - ); - } - - return { lesionNamingNumber: item.lesionNamingNumber }; - }); - } - - getFirstMeasurement(timepointId) { - // Get child tools from all included tool groups - let childTools = []; - configuration.measurementTools.forEach(toolGroup => { - // Skip the tool groups excluded from case progress - if (!MeasurementApi.isToolIncluded(toolGroup)) { - return false; - } - - childTools = childTools.concat(toolGroup.childTools); - }); - - // Get all included child tools - const includedChildTools = childTools.filter(tool => - MeasurementApi.isToolIncluded(tool) - ); - - // Get the first measurement for the given timepoint - let measurement = undefined; - includedChildTools.every(tool => { - measurement = this.tools[tool.id].find( - t => t.timepointId === timepointId && t.measurementNumber === 1 - ); - - return !measurement; - }); - - // Return the found measurement object or undefined if not found - return measurement; - } - - lesionExistsAtTimepoints(lesionNamingNumber, toolGroupId, timepointIds) { - // Retrieve all the data for the given tool group (e.g. 'targets') - const measurementsAtTimepoint = this.fetch(toolGroupId, tool => - timepointIds.includes(tool.timepointId) - ); - - // Return whether or not any lesion at this timepoint has the same lesionNamingNumber - return !!measurementsAtTimepoint.find( - m => m.lesionNamingNumber === lesionNamingNumber - ); - } - - isNewLesionsMeasurement(measurementData) { - if (!measurementData) { - return; - } - - const toolConfig = MeasurementApi.getToolConfiguration( - measurementData.toolType - ); - const toolType = toolConfig.tool.parentTool || measurementData.toolType; - const { timepointApi } = this; - const currentMeasurement = - this.tools[toolType].find(tool => tool._id === measurementData._id) || {}; - const timepointId = - currentMeasurement.timepointId || measurementData.timepointId; - const lesionNamingNumber = - currentMeasurement.lesionNamingNumber || - measurementData.lesionNamingNumber; - - // Stop here if the needed information is not set - if (!timepointApi || !timepointId || !toolConfig) { - return; - } - - const { toolGroupId } = toolConfig; - const current = timepointApi.timepoints.find( - tp => tp.timepointId === timepointId - ); - const initialTimepointIds = timepointApi.initialTimepointIds(); - - // Stop here if there's no initial timepoint, or if the current is any initial - if ( - !initialTimepointIds || - initialTimepointIds.length < 1 || - initialTimepointIds.some( - initialtpid => initialtpid === current.timepointId - ) - ) { - return false; - } - - return ( - this.lesionExistsAtTimepoints( - lesionNamingNumber, - toolGroupId, - initialTimepointIds - ) === false - ); - } - - calculateLesionMaxMeasurementNumber(groupId, filter) { - let measurements = []; - if (groupId) { - // Get the measurements of the group - measurements = this.toolGroups[groupId] || []; - } else { - // Get all measurements of all groups - measurements = Object.keys(this.toolGroups).reduce((acc, val) => { - acc.push(...this.toolGroups[val]); - return acc; - }, []); - } - - const sortedMeasurements = measurements.filter(filter).sort((tp1, tp2) => { - return tp1.measurementNumber < tp2.measurementNumber ? 1 : -1; - }); - - for (let i = 0; i < sortedMeasurements.length; i++) { - const toolGroupMeasurement = sortedMeasurements[i]; - const measurement = this.tools[toolGroupMeasurement.toolId].find( - tool => tool._id === toolGroupMeasurement.toolItemId - ); - const isNew = this.isNewLesionsMeasurement(measurement); - if (!isNew) { - return measurement.measurementNumber; - } - } - - return 0; - } - - calculateNewLesionMaxMeasurementNumber(groupId, filter) { - const sortedMeasurements = this.toolGroups[groupId] - .filter(filter) - .sort((tp1, tp2) => { - return tp1.measurementNumber < tp2.measurementNumber ? 1 : -1; - }); - - for (let i = 0; i < sortedMeasurements.length; i++) { - const toolGroupMeasurement = sortedMeasurements[i]; - const measurement = this.tools[toolGroupMeasurement.toolId].find( - tool => tool._id === toolGroupMeasurement.toolItemId - ); - const isNew = this.isNewLesionsMeasurement(measurement); - if (isNew) { - return measurement.measurementNumber; - } - } - - return 0; - } - - calculateMeasurementNumber(measurement) { - const toolGroupId = this.toolsGroupsMap[measurement.toolType]; - - const filter = tool => tool._id !== measurement._id; - - const isNew = this.isNewLesionsMeasurement(measurement); - - if (isNew) { - const maxTargetMeasurementNumber = this.calculateLesionMaxMeasurementNumber( - 'targets', - filter - ); - const maxNonTargetMeasurementNumber = this.calculateLesionMaxMeasurementNumber( - 'nonTargets', - filter - ); - const maxNewTargetMeasurementNumber = this.calculateNewLesionMaxMeasurementNumber( - 'targets', - filter - ); - if (toolGroupId === 'targets') { - return Math.max( - maxTargetMeasurementNumber, - maxNonTargetMeasurementNumber, - maxNewTargetMeasurementNumber - ); - } else if (toolGroupId === 'nonTargets') { - const maxNewNonTargetMeasurementNumber = this.calculateNewLesionMaxMeasurementNumber( - 'nonTargets', - filter - ); - return Math.max( - maxTargetMeasurementNumber, - maxNonTargetMeasurementNumber, - maxNewTargetMeasurementNumber, - maxNewNonTargetMeasurementNumber - ); - } - } else { - const maxTargetMeasurementNumber = this.calculateLesionMaxMeasurementNumber( - 'targets', - filter - ); - if (toolGroupId === 'targets') { - return maxTargetMeasurementNumber; - } else if (toolGroupId === 'nonTargets') { - const maxNonTargetMeasurementNumber = this.calculateLesionMaxMeasurementNumber( - 'nonTargets', - filter - ); - return Math.max( - maxTargetMeasurementNumber, - maxNonTargetMeasurementNumber - ); - } else { - return this.calculateLesionMaxMeasurementNumber(null, filter); - } - } - - return 0; - } - - getPreviousMeasurement(measurementData) { - if (!measurementData) { - return; - } - - const { timepointId, toolType, lesionNamingNumber } = measurementData; - if (!timepointId || !toolType || !lesionNamingNumber) { - return; - } - - const toolGroupId = this.toolsGroupsMap[measurementData.toolType]; - - // TODO: Remove TrialPatientLocationUID from here and override it somehow - // by dependant applications. Here we should use the location attribute instead of the uid - let filter; - const uid = - measurementData.additionalData && - measurementData.additionalData.TrialPatientLocationUID; - if (uid) { - filter = tool => - tool._id !== measurementData._id && - tool.additionalData && - tool.additionalData.TrialPatientLocationUID === uid; - } else { - filter = tool => - tool._id !== measurementData._id && - tool.lesionNamingNumber === lesionNamingNumber; - } - - const childToolTypes = this.toolGroupTools[toolGroupId]; - for (let i = 0; i < childToolTypes.length; i++) { - const childToolType = childToolTypes[i]; - const toolCollection = this.tools[childToolType]; - const item = toolCollection.find(filter); - - if (item) { - return item; - } - } - } - - hasDuplicateMeasurementNumber(measurementData) { - if (!measurementData) { - return; - } - - const { toolType, measurementNumber } = measurementData; - if (!toolType || !measurementNumber) { - return; - } - - const filter = tool => - tool._id !== measurementData._id && - tool.measurementNumber === measurementData.measurementNumber; - - return configuration.measurementTools - .filter(toolGroup => toolGroup.id !== 'temp') - .some(toolGroup => { - if (this.toolGroups[toolGroup.id].find(filter)) { - return true; - } - return toolGroup.childTools.some(tool => { - if (this.tools[tool.id].find(filter)) { - return true; - } - }); - }); - } - - updateNumbering(collectionToUpdate, propertyFilter, propertyName, increment) { - collectionToUpdate.filter(propertyFilter).forEach(item => { - item[propertyName] += increment; - }); - } - - updateMeasurementNumberForAllMeasurements(measurement, increment) { - const filter = tool => - tool._id !== measurement._id && - tool.measurementNumber >= measurement.measurementNumber; - - configuration.measurementTools - .filter(toolGroup => toolGroup.id !== 'temp') - .forEach(toolGroup => { - this.updateNumbering( - this.toolGroups[toolGroup.id], - filter, - 'measurementNumber', - increment - ); - - toolGroup.childTools.forEach(tool => { - this.updateNumbering( - this.tools[tool.id], - filter, - 'measurementNumber', - increment - ); - }); - }); - } - - addMeasurement(toolType, measurement) { - const toolGroup = this.toolsGroupsMap[toolType]; - const groupCollection = this.toolGroups[toolGroup]; - const collection = this.tools[toolType]; - - // Get the related measurement by the measurement number and use its location if defined - const relatedMeasurement = collection.find( - t => - t.lesionNamingNumber === measurement.lesionNamingNumber && - t.toolType === measurement.toolType - ); - - // Use the related measurement location if found and defined - if (relatedMeasurement && relatedMeasurement.location) { - measurement.location = relatedMeasurement.location; - } - - // Use the related measurement description if found and defined - if (relatedMeasurement && relatedMeasurement.description) { - measurement.description = relatedMeasurement.description; - } - - measurement._id = guid(); - - // Get the timepoint - let timepoint; - if (measurement.StudyInstanceUID) { - timepoint = this.timepointApi.study(measurement.StudyInstanceUID)[0]; - } else { - const { timepointId } = measurement; - timepoint = this.timepointApi.timepoints.find( - t => t.timepointId === timepointId - ); - } - - // Preventing errors thrown when non-associated (standalone) study is opened... - // @TODO: Make sure this logic is correct. - if (!timepoint) return; - - // Empty Item is the lesion just added in cornerstoneTools, but does not have measurement data yet - const emptyItem = groupCollection.find( - groupTool => - !groupTool.toolId && groupTool.timepointId === timepoint.timepointId - ); - - // Set the timepointId attribute to measurement to make it easier to filter measurements by timepoint - measurement.timepointId = timepoint.timepointId; - - // Check if the measurement data is just added by a cornerstone tool and is still empty - if (emptyItem) { - // Set relevant initial data and measurement number to the measurement - measurement.lesionNamingNumber = emptyItem.lesionNamingNumber; - measurement.measurementNumber = emptyItem.measurementNumber; - - groupCollection - .filter( - groupTool => - groupTool.timepointId === timepoint.timepointId && - groupTool.lesionNamingNumber === measurement.lesionNamingNumber - ) - .forEach(groupTool => { - groupTool.toolId = tool.id; - groupTool.toolItemId = measurement._id; - groupTool.createdAt = measurement.createdAt; - groupTool.measurementNumber = measurement.measurementNumber; - }); - } else { - // Handle measurements not added by cornerstone tools and update its number - const measurementsInTimepoint = groupCollection.filter( - groupTool => groupTool.timepointId === timepoint.timepointId - ); - measurement.lesionNamingNumber = this.calculateLesionNamingNumber( - measurementsInTimepoint - ); - measurement.measurementNumber = - measurement.measurementNumber || - this.calculateMeasurementNumber(measurement) + 1; - } - - // Define an update object to reflect the changes in the collection - const updateObject = { - timepointId: timepoint.timepointId, - lesionNamingNumber: measurement.lesionNamingNumber, - measurementNumber: measurement.measurementNumber, - }; - - // Find the matched measurement from other timepoints - const found = this.getPreviousMeasurement(measurement); - - // Check if a previous related meausurement was found on other timepoints - if (found) { - // Use the same number as the previous measurement - measurement.lesionNamingNumber = found.lesionNamingNumber; - measurement.measurementNumber = found.measurementNumber; - - // TODO: Remove TrialPatientLocationUID from here and override it somehow - // by dependant applications - - // Change the update object to set the same number, additionalData, - // location, label and description to the current measurement - updateObject.lesionNamingNumber = found.lesionNamingNumber; - updateObject.measurementNumber = found.measurementNumber; - updateObject.additionalData = measurement.additionalData || {}; - updateObject.additionalData.TrialPatientLocationUID = - found.additionalData && found.additionalData.TrialPatientLocationUID; - updateObject.location = found.location; - updateObject.label = found.label; - updateObject.description = found.description; - updateObject.isSplitLesion = found.isSplitLesion; - updateObject.isNodal = found.isNodal; - - const description = getDescription(found, measurement); - if (description) { - updateObject.description = description; - } - } else if (this.hasDuplicateMeasurementNumber(measurement)) { - // Update measurementNumber for the measurements with masurementNumber greater or equal than - // measurementNumber of the added measurement (except the added one) - // only if there is another measurement with the same measurementNumber - this.updateMeasurementNumberForAllMeasurements(measurement, 1); - } - - let addedMeasurement; - - // Upsert the measurement in collection - const toolIndex = collection.findIndex( - tool => tool._id === measurement._id - ); - if (toolIndex > -1) { - addedMeasurement = Object.assign({}, collection[toolIndex], updateObject); - collection[toolIndex] = addedMeasurement; - } else { - addedMeasurement = Object.assign({}, measurement, updateObject); - collection.push(addedMeasurement); - } - - if (!emptyItem) { - // Reflect the entry in the tool group collection - groupCollection.push({ - toolId: toolType, - toolItemId: addedMeasurement._id, - timepointId: timepoint.timepointId, - StudyInstanceUID: addedMeasurement.StudyInstanceUID, - createdAt: addedMeasurement.createdAt, - lesionNamingNumber: addedMeasurement.lesionNamingNumber, - measurementNumber: addedMeasurement.measurementNumber, - }); - } - - // Let others know that the measurements are updated - this.onMeasurementsUpdated(); - - // TODO: Enable reactivity - // this.timepointChanged.set(timepoint.timepointId); - - return addedMeasurement; - } - - updateMeasurement(toolType, measurement) { - const collection = this.tools[toolType]; - - const toolIndex = collection.findIndex( - tool => tool._id === measurement._id - ); - if (toolIndex < 0) { - return; - } - - collection[toolIndex] = Object.assign({}, measurement); - - // Let others know that the measurements are updated - this.onMeasurementsUpdated(); - - // TODO: Enable reactivity - // this.timepointChanged.set(timepoint.timepointId); - } - - onMeasurementRemoved(toolType, measurement) { - const { lesionNamingNumber, measurementNumber } = measurement; - - const toolGroupId = this.toolsGroupsMap[toolType]; - const groupCollection = this.toolGroups[toolGroupId]; - - const groupIndex = groupCollection.findIndex( - group => group.toolItemId === measurement._id - ); - if (groupIndex < 0) { - return; - } - - // Remove the deleted measurement only in its timepoint from the collection - groupCollection.splice(groupIndex, 1); - - // Check which timepoints have the deleted measurement - const timepointsWithDeletedMeasurement = groupCollection - .filter(tool => tool.measurementNumber === measurementNumber) - .map(tool => tool.timepointId); - - // Update lesionNamingNumber and measurementNumber only if there is no timepoint with that measurement - if (timepointsWithDeletedMeasurement.length < 1) { - // Decrease lesionNamingNumber of all measurements with lesionNamingNumber greater than lesionNamingNumber of the deleted measurement by 1 - const lesionNamingNumberFilter = tool => - tool.lesionNamingNumber >= lesionNamingNumber; - this.updateNumbering( - groupCollection, - lesionNamingNumberFilter, - 'lesionNamingNumber', - -1 - ); - - const toolGroup = configuration.measurementTools.find( - tGroup => tGroup.id === toolGroupId - ); - if (toolGroup && toolGroup.childTools) { - toolGroup.childTools.forEach(childTool => { - const collection = this.tools[childTool.id]; - this.updateNumbering( - collection, - lesionNamingNumberFilter, - 'lesionNamingNumber', - -1 - ); - }); - } - - // Decrease measurementNumber of all measurements with measurementNumber greater than measurementNumber of the deleted measurement by 1 - this.updateMeasurementNumberForAllMeasurements(measurement, -1); - } - - // Synchronize the new tool data - this.syncMeasurementsAndToolData(); - - // Let others know that the measurements are updated - this.onMeasurementsUpdated(); - - // TODO: Enable reactivity - // this.timepointChanged.set(timepoint.timepointId); - } - - syncMeasurementsAndToolData() { - configuration.measurementTools.forEach(toolGroup => { - // Skip the tool groups excluded from case progress - if (!MeasurementApi.isToolIncluded(toolGroup)) { - return; - } - toolGroup.childTools.forEach(tool => { - // Skip the tools excluded from case progress - if (!MeasurementApi.isToolIncluded(tool)) { - return; - } - const measurements = this.tools[tool.id]; - measurements.forEach(measurement => { - MeasurementApi.syncMeasurementAndToolData(measurement); - }); - }); - }); - } - - deleteMeasurements(toolType, measurementTypeId, filter) { - const filterKeys = Object.keys(filter); - const groupCollection = this.toolGroups[measurementTypeId]; - - // Stop here if it is a temporary toolGroups - if (!groupCollection) return; - - // Get the entries information before removing them - const groupItems = groupCollection.filter(toolGroup => { - return filterKeys.every( - filterKey => toolGroup[filterKey] === filter[filterKey] - ); - }); - const entries = []; - groupItems.forEach(groupItem => { - if (!groupItem.toolId) { - return; - } - - const collection = this.tools[groupItem.toolId]; - const toolIndex = collection.findIndex( - tool => tool._id === groupItem.toolItemId - ); - if (toolIndex > -1) { - entries.push(collection[toolIndex]); - collection.splice(toolIndex, 1); - } - }); - - // Stop here if no entries were found - if (!entries.length) { - return; - } - - // If the filter doesn't have the measurement number, get it from the first entry - const lesionNamingNumber = - filter.lesionNamingNumber || entries[0].lesionNamingNumber; - - // Synchronize the new data with cornerstone tools - const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState(); - - entries.forEach(entry => { - const measurementsData = []; - const { tool } = MeasurementApi.getToolConfiguration(entry.toolType); - if (Array.isArray(tool.childTools)) { - tool.childTools.forEach(key => { - const childMeasurement = entry[key]; - if (!childMeasurement) return; - measurementsData.push(childMeasurement); - }); - } else { - measurementsData.push(entry); - } - - measurementsData.forEach(measurementData => { - const { imagePath, toolType } = measurementData; - const imageId = getImageIdForImagePath(imagePath); - if (imageId && toolState[imageId]) { - const toolData = toolState[imageId][toolType]; - const measurementEntries = toolData && toolData.data; - const measurementEntry = measurementEntries.find( - mEntry => mEntry._id === entry._id - ); - if (measurementEntry) { - const index = measurementEntries.indexOf(measurementEntry); - measurementEntries.splice(index, 1); - } - } - }); - - this.onMeasurementRemoved(toolType, entry); - }); - - cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState( - toolState - ); - - // Synchronize the updated measurements with Cornerstone Tools - // toolData to make sure the displayed measurements show 'Target X' correctly - const syncFilter = Object.assign({}, filter); - delete syncFilter.timepointId; - delete syncFilter.lesionNamingNumber; - - const syncFilterKeys = Object.keys(syncFilter); - - const toolTypes = [...new Set(entries.map(entry => entry.toolType))]; - toolTypes.forEach(toolType => { - const collection = this.tools[toolType]; - collection - .filter(tool => { - return ( - tool.lesionNamingNumber > lesionNamingNumber - 1 && - syncFilterKeys.every( - syncFilterKey => tool[syncFilterKey] === filter[syncFilterKey] - ) - ); - }) - .forEach(measurement => { - MeasurementApi.syncMeasurementAndToolData(measurement); - }); - }); - } -} diff --git a/platform/core/src/measurements/classes/TimepointApi.js b/platform/core/src/measurements/classes/TimepointApi.js deleted file mode 100644 index 6719ac0038b..00000000000 --- a/platform/core/src/measurements/classes/TimepointApi.js +++ /dev/null @@ -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}`; - } -} diff --git a/platform/core/src/measurements/classes/index.js b/platform/core/src/measurements/classes/index.js deleted file mode 100644 index 4cd734312b4..00000000000 --- a/platform/core/src/measurements/classes/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import MeasurementApi from './MeasurementApi'; -import TimepointApi from './TimepointApi'; - -export { TimepointApi, MeasurementApi }; diff --git a/platform/core/src/measurements/configuration.js b/platform/core/src/measurements/configuration.js deleted file mode 100644 index c8e15247713..00000000000 --- a/platform/core/src/measurements/configuration.js +++ /dev/null @@ -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 }; diff --git a/platform/core/src/measurements/conformance/CriteriaEvaluator.js b/platform/core/src/measurements/conformance/CriteriaEvaluator.js deleted file mode 100644 index bddfb76b6fb..00000000000 --- a/platform/core/src/measurements/conformance/CriteriaEvaluator.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/measurements/conformance/criteria/BaseCriterion.js b/platform/core/src/measurements/conformance/criteria/BaseCriterion.js deleted file mode 100644 index 0b88eca7f26..00000000000 --- a/platform/core/src/measurements/conformance/criteria/BaseCriterion.js +++ /dev/null @@ -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; - } -} diff --git a/platform/core/src/measurements/conformance/criteria/Location.js b/platform/core/src/measurements/conformance/criteria/Location.js deleted file mode 100644 index 208026c784d..00000000000 --- a/platform/core/src/measurements/conformance/criteria/Location.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/MaxTargets.js b/platform/core/src/measurements/conformance/criteria/MaxTargets.js deleted file mode 100644 index 40dcec80004..00000000000 --- a/platform/core/src/measurements/conformance/criteria/MaxTargets.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/MaxTargetsPerOrgan.js b/platform/core/src/measurements/conformance/criteria/MaxTargetsPerOrgan.js deleted file mode 100644 index 2b09434ec20..00000000000 --- a/platform/core/src/measurements/conformance/criteria/MaxTargetsPerOrgan.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/MeasurementsLength.js b/platform/core/src/measurements/conformance/criteria/MeasurementsLength.js deleted file mode 100644 index 2cbbf4e5a76..00000000000 --- a/platform/core/src/measurements/conformance/criteria/MeasurementsLength.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/Modality.js b/platform/core/src/measurements/conformance/criteria/Modality.js deleted file mode 100644 index 2f407992846..00000000000 --- a/platform/core/src/measurements/conformance/criteria/Modality.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/NonTargetResponse.js b/platform/core/src/measurements/conformance/criteria/NonTargetResponse.js deleted file mode 100644 index f8b4f966551..00000000000 --- a/platform/core/src/measurements/conformance/criteria/NonTargetResponse.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/TargetType.js b/platform/core/src/measurements/conformance/criteria/TargetType.js deleted file mode 100644 index b40f63962e0..00000000000 --- a/platform/core/src/measurements/conformance/criteria/TargetType.js +++ /dev/null @@ -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); - } -} diff --git a/platform/core/src/measurements/conformance/criteria/index.js b/platform/core/src/measurements/conformance/criteria/index.js deleted file mode 100644 index 5501e2c7a19..00000000000 --- a/platform/core/src/measurements/conformance/criteria/index.js +++ /dev/null @@ -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'; diff --git a/platform/core/src/measurements/conformance/evaluations/index.js b/platform/core/src/measurements/conformance/evaluations/index.js deleted file mode 100644 index ffb8fbb7272..00000000000 --- a/platform/core/src/measurements/conformance/evaluations/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import * as recistEvaluation from './recist.json'; - -export const recist11 = recistEvaluation; diff --git a/platform/core/src/measurements/conformance/evaluations/recist.json b/platform/core/src/measurements/conformance/evaluations/recist.json deleted file mode 100644 index fe93839d67f..00000000000 --- a/platform/core/src/measurements/conformance/evaluations/recist.json +++ /dev/null @@ -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": {} -} diff --git a/platform/core/src/measurements/conformance/index.js b/platform/core/src/measurements/conformance/index.js deleted file mode 100644 index 901d433e76c..00000000000 --- a/platform/core/src/measurements/conformance/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ConformanceCriteria from './ConformanceCriteria'; - -export { ConformanceCriteria }; diff --git a/platform/core/src/measurements/dataExchange.js b/platform/core/src/measurements/dataExchange.js deleted file mode 100644 index c584c30d9a3..00000000000 --- a/platform/core/src/measurements/dataExchange.js +++ /dev/null @@ -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(); -}; diff --git a/platform/core/src/measurements/index.js b/platform/core/src/measurements/index.js deleted file mode 100644 index 20b55b6fe50..00000000000 --- a/platform/core/src/measurements/index.js +++ /dev/null @@ -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; diff --git a/platform/core/src/measurements/lib/getDescription.js b/platform/core/src/measurements/lib/getDescription.js deleted file mode 100644 index e6e83018c04..00000000000 --- a/platform/core/src/measurements/lib/getDescription.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function(measurement) { - return measurement.description; -} diff --git a/platform/core/src/measurements/lib/getImageAttributes.js b/platform/core/src/measurements/lib/getImageAttributes.js deleted file mode 100644 index e2759a6750d..00000000000 --- a/platform/core/src/measurements/lib/getImageAttributes.js +++ /dev/null @@ -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, - }; -} diff --git a/platform/core/src/measurements/lib/getImageIdForImagePath.js b/platform/core/src/measurements/lib/getImageIdForImagePath.js deleted file mode 100644 index 953811d5d0d..00000000000 --- a/platform/core/src/measurements/lib/getImageIdForImagePath.js +++ /dev/null @@ -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); -} diff --git a/platform/core/src/measurements/lib/getLabel.js b/platform/core/src/measurements/lib/getLabel.js deleted file mode 100644 index cfb10363ffb..00000000000 --- a/platform/core/src/measurements/lib/getLabel.js +++ /dev/null @@ -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}`; - } -} diff --git a/platform/core/src/measurements/ltTools.js b/platform/core/src/measurements/ltTools.js deleted file mode 100644 index c99bced6368..00000000000 --- a/platform/core/src/measurements/ltTools.js +++ /dev/null @@ -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; diff --git a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementAdded.js b/platform/core/src/measurements/measurementHandlers/handleChildMeasurementAdded.js deleted file mode 100644 index 35955e7a44f..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementAdded.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementModified.js b/platform/core/src/measurements/measurementHandlers/handleChildMeasurementModified.js deleted file mode 100644 index 05d44777d86..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementModified.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementRemoved.js b/platform/core/src/measurements/measurementHandlers/handleChildMeasurementRemoved.js deleted file mode 100644 index 21ae8b9d465..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleChildMeasurementRemoved.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementAdded.js b/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementAdded.js deleted file mode 100644 index 55ecc7b5f25..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementAdded.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementModified.js b/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementModified.js deleted file mode 100644 index 23c5b08d606..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementModified.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementRemoved.js b/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementRemoved.js deleted file mode 100644 index 7faca56f995..00000000000 --- a/platform/core/src/measurements/measurementHandlers/handleSingleMeasurementRemoved.js +++ /dev/null @@ -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 - } -} diff --git a/platform/core/src/measurements/measurementHandlers/index.js b/platform/core/src/measurements/measurementHandlers/index.js deleted file mode 100644 index 7f01f690a56..00000000000 --- a/platform/core/src/measurements/measurementHandlers/index.js +++ /dev/null @@ -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; diff --git a/platform/core/src/measurements/toolGroups/allTools.js b/platform/core/src/measurements/toolGroups/allTools.js deleted file mode 100644 index 0aa6a6ab1f4..00000000000 --- a/platform/core/src/measurements/toolGroups/allTools.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/toolGroups/nonTargets.js b/platform/core/src/measurements/toolGroups/nonTargets.js deleted file mode 100644 index 95492443006..00000000000 --- a/platform/core/src/measurements/toolGroups/nonTargets.js +++ /dev/null @@ -1,13 +0,0 @@ -import { nonTarget } from '../tools'; - -export const nonTargets = { - id: 'nonTargets', - name: 'Non-Targets', - childTools: [nonTarget], - options: { - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/toolGroups/targets.js b/platform/core/src/measurements/toolGroups/targets.js deleted file mode 100644 index 29d3bec5cae..00000000000 --- a/platform/core/src/measurements/toolGroups/targets.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/toolGroups/temp.js b/platform/core/src/measurements/toolGroups/temp.js deleted file mode 100644 index 228486e0a52..00000000000 --- a/platform/core/src/measurements/toolGroups/temp.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/angle.js b/platform/core/src/measurements/tools/angle.js deleted file mode 100644 index 4fc1a05f474..00000000000 --- a/platform/core/src/measurements/tools/angle.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/arrowAnnotate.js b/platform/core/src/measurements/tools/arrowAnnotate.js deleted file mode 100644 index a45822addb2..00000000000 --- a/platform/core/src/measurements/tools/arrowAnnotate.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/bidirectional.js b/platform/core/src/measurements/tools/bidirectional.js deleted file mode 100644 index e17955070b9..00000000000 --- a/platform/core/src/measurements/tools/bidirectional.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/circleRoi.js b/platform/core/src/measurements/tools/circleRoi.js deleted file mode 100644 index 6be0f396aff..00000000000 --- a/platform/core/src/measurements/tools/circleRoi.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/ellipticalRoi.js b/platform/core/src/measurements/tools/ellipticalRoi.js deleted file mode 100644 index ba6a070f430..00000000000 --- a/platform/core/src/measurements/tools/ellipticalRoi.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/freehandMouse.js b/platform/core/src/measurements/tools/freehandMouse.js deleted file mode 100644 index 83ad3ee2d46..00000000000 --- a/platform/core/src/measurements/tools/freehandMouse.js +++ /dev/null @@ -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, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/index.js b/platform/core/src/measurements/tools/index.js deleted file mode 100644 index 16035e2215b..00000000000 --- a/platform/core/src/measurements/tools/index.js +++ /dev/null @@ -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, -}; diff --git a/platform/core/src/measurements/tools/length.js b/platform/core/src/measurements/tools/length.js deleted file mode 100644 index ccad2fa0144..00000000000 --- a/platform/core/src/measurements/tools/length.js +++ /dev/null @@ -1,23 +0,0 @@ -const displayFunction = data => { - let lengthValue = ''; - if (data.length && !isNaN(data.length)) { - lengthValue = data.length.toFixed(2) + ' mm'; - } - return lengthValue; -}; - -export const length = { - id: 'Length', - name: 'Length', - toolGroup: 'allTools', - cornerstoneToolType: 'Length', - options: { - measurementTable: { - displayFunction, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/nonTarget.js b/platform/core/src/measurements/tools/nonTarget.js deleted file mode 100644 index 7e0848018c4..00000000000 --- a/platform/core/src/measurements/tools/nonTarget.js +++ /dev/null @@ -1,15 +0,0 @@ -export const nonTarget = { - id: 'NonTarget', - name: 'Non-Target', - toolGroup: 'allTools', - cornerstoneToolType: 'NonTarget', - options: { - measurementTable: { - displayFunction: data => data.response, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/rectangleRoi.js b/platform/core/src/measurements/tools/rectangleRoi.js deleted file mode 100644 index 5ac159b6bf5..00000000000 --- a/platform/core/src/measurements/tools/rectangleRoi.js +++ /dev/null @@ -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 rectangleRoi = { - id: 'RectangleRoi', - name: 'Rectangle', - toolGroup: 'allTools', - cornerstoneToolType: 'RectangleRoi', - options: { - measurementTable: { - displayFunction, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/targetCR.js b/platform/core/src/measurements/tools/targetCR.js deleted file mode 100644 index fffd29f25a4..00000000000 --- a/platform/core/src/measurements/tools/targetCR.js +++ /dev/null @@ -1,15 +0,0 @@ -export const targetCR = { - id: 'TargetCR', - name: 'CR Target', - toolGroup: 'allTools', - cornerstoneToolType: 'TargetCR', - options: { - measurementTable: { - displayFunction: data => data.response, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/targetNE.js b/platform/core/src/measurements/tools/targetNE.js deleted file mode 100644 index 361de89d07e..00000000000 --- a/platform/core/src/measurements/tools/targetNE.js +++ /dev/null @@ -1,15 +0,0 @@ -export const targetNE = { - id: 'TargetNE', - name: 'NE Target', - toolGroup: 'allTools', - cornerstoneToolType: 'TargetNE', - options: { - measurementTable: { - displayFunction: data => data.response, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/measurements/tools/targetUN.js b/platform/core/src/measurements/tools/targetUN.js deleted file mode 100644 index 3bf8acdc7f2..00000000000 --- a/platform/core/src/measurements/tools/targetUN.js +++ /dev/null @@ -1,15 +0,0 @@ -export const targetUN = { - id: 'TargetUN', - name: 'UN Target', - toolGroup: 'allTools', - cornerstoneToolType: 'TargetUN', - options: { - measurementTable: { - displayFunction: data => data.response, - }, - caseProgress: { - include: true, - evaluate: true, - }, - }, -}; diff --git a/platform/core/src/redux/actions.js b/platform/core/src/redux/actions.js deleted file mode 100644 index 1e46a9c4d9c..00000000000 --- a/platform/core/src/redux/actions.js +++ /dev/null @@ -1,147 +0,0 @@ -/** Action Creators: - * https://redux.js.org/basics/actions#action-creators - */ - -import { - CLEAR_VIEWPORT, - SET_ACTIVE_SPECIFIC_DATA, - SET_SERVERS, - SET_VIEWPORT, - SET_VIEWPORT_ACTIVE, - SET_VIEWPORT_LAYOUT, - SET_VIEWPORT_LAYOUT_AND_DATA, - SET_USER_PREFERENCES, -} from './constants/ActionTypes.js'; - -/** - * The definition of a viewport layout. - * - * @typedef {Object} ViewportLayout - * @property {number} numRows - - * @property {number} numColumns - - * @property {array} viewports - - */ - -/** - * VIEWPORT - */ -export const setViewportSpecificData = ( - viewportIndex, - viewportSpecificData -) => ({ - type: SET_VIEWPORT, - viewportIndex, - viewportSpecificData, -}); - -export const setViewportActive = viewportIndex => ({ - type: SET_VIEWPORT_ACTIVE, - viewportIndex, -}); - -/** - * @param {ViewportLayout} layout - */ -export const setLayout = ({ numRows, numColumns, viewports }) => ({ - type: SET_VIEWPORT_LAYOUT, - numRows, - numColumns, - viewports, -}); - -/** - * @param {number} layout.numRows - * @param {number} layout.numColumns - * @param {array} viewports - */ -export const setViewportLayoutAndData = ( - { numRows, numColumns, viewports }, - viewportSpecificData -) => ({ - type: SET_VIEWPORT_LAYOUT_AND_DATA, - numRows, - numColumns, - viewports, - viewportSpecificData, -}); - -export const clearViewportSpecificData = viewportIndex => ({ - type: CLEAR_VIEWPORT, - viewportIndex, -}); - -export const setActiveViewportSpecificData = viewportSpecificData => ({ - type: SET_ACTIVE_SPECIFIC_DATA, - viewportSpecificData, -}); - -/** - * NOT-VIEWPORT - */ -export const setStudyLoadingProgress = (progressId, progressData) => ({ - type: 'SET_STUDY_LOADING_PROGRESS', - progressId, - progressData, -}); - -export const clearStudyLoadingProgress = progressId => ({ - type: 'CLEAR_STUDY_LOADING_PROGRESS', - progressId, -}); - -export const setUserPreferences = state => ({ - type: SET_USER_PREFERENCES, - state, -}); - -export const setExtensionData = (extension, data) => ({ - type: 'SET_EXTENSION_DATA', - extension, - data, -}); - -export const setTimepoints = state => ({ - type: 'SET_TIMEPOINTS', - state, -}); - -export const setMeasurements = state => ({ - type: 'SET_MEASUREMENTS', - state, -}); - -export const setStudyData = (StudyInstanceUID, data) => ({ - type: 'SET_STUDY_DATA', - StudyInstanceUID, - data, -}); - -export const setServers = servers => ({ - type: SET_SERVERS, - servers, -}); - -const actions = { - /** - * VIEWPORT - */ - setViewportActive, - setViewportSpecificData, - setViewportLayoutAndData, - setLayout, - clearViewportSpecificData, - setActiveViewportSpecificData, - /** - * NOT-VIEWPORT - */ - setStudyLoadingProgress, - clearStudyLoadingProgress, - setUserPreferences, - setExtensionData, - setTimepoints, - setMeasurements, - setStudyData, - setServers, -}; - -export default actions; diff --git a/platform/core/src/redux/actions.test.js b/platform/core/src/redux/actions.test.js deleted file mode 100644 index e80297a667b..00000000000 --- a/platform/core/src/redux/actions.test.js +++ /dev/null @@ -1,113 +0,0 @@ -import * as types from './constants/ActionTypes.js'; - -import actions from './actions.js'; - -describe('actions', () => { - test('exports have not changed', () => { - const expectedExports = [ - 'setViewportActive', - 'setViewportSpecificData', - 'setViewportLayoutAndData', - 'setLayout', - 'clearViewportSpecificData', - 'setActiveViewportSpecificData', - 'setStudyLoadingProgress', - 'clearStudyLoadingProgress', - 'setUserPreferences', - 'setExtensionData', - 'setTimepoints', - 'setMeasurements', - 'setServers', - 'setStudyData', - ].sort(); - - const exports = Object.keys(actions).sort(); - - expect(exports).toEqual(expectedExports); - }); - - describe('viewport action creators', () => { - it('should create an action to set the viewport specific data', () => { - const viewportSpecificData = { - displaySetInstanceUID: 'ef859a23-4631-93ab-d26b-7940a822c699', - SeriesDate: '20151026', - SeriesTime: '082611.370000', - SeriesInstanceUID: - '1.3.6.1.4.1.25403.345050719074.3824.20170126085406.5', - SeriesNumber: 2, - SeriesDescription: 'Chest 3x3 Soft', - numImageFrames: 126, - Modality: 'CT', - isMultiFrame: false, - InstanceNumber: 1, - StudyInstanceUID: - '1.3.6.1.4.1.25403.345050719074.3824.20170126085406.1', - sopClassUIDs: ['1.2.840.10008.5.1.4.1.1.2'], - plugin: 'cornerstone', - viewport: { - zoomScale: null, - rotation: 360, - resetViewport: null, - invert: null, - vflip: null, - hflip: null, - clearTools: null, - scrollUp: null, - scrollDown: null, - scrollFirstImage: null, - scrollLastImage: null, - previousPanel: null, - nextPanel: null, - nextSeries: null, - previousSeries: null, - }, - }; - - const expectedAction = { - type: types.SET_ACTIVE_SPECIFIC_DATA, - viewportSpecificData, - }; - - expect( - actions.setActiveViewportSpecificData(viewportSpecificData) - ).toEqual(expectedAction); - }); - - it('should create an action to clear clearViewportSpecificData', () => { - const viewportIndex = 1; - const expectedAction = { - type: types.CLEAR_VIEWPORT, - viewportIndex, - }; - expect(actions.clearViewportSpecificData(viewportIndex)).toEqual( - expectedAction - ); - }); - - it('should create an action to set the active viewport', () => { - const viewportIndex = 1; - const expectedAction = { - type: types.SET_VIEWPORT_ACTIVE, - viewportIndex, - }; - expect(actions.setViewportActive(viewportIndex)).toEqual(expectedAction); - }); - - it('should create an action to set the viewport layout', () => { - const numRows = 1; - const numColumns = 2; - const viewports = [{ plugin: 'vtk' }, { plugin: 'vtk' }]; - - const expectedAction = { - type: types.SET_VIEWPORT_LAYOUT, - numRows, - numColumns, - viewports, - }; - - expect(actions.setLayout({ numRows, numColumns, viewports })).toEqual( - expectedAction - ); - }); - }); -}); diff --git a/platform/core/src/redux/constants/ActionTypes.js b/platform/core/src/redux/constants/ActionTypes.js deleted file mode 100644 index e5167beb937..00000000000 --- a/platform/core/src/redux/constants/ActionTypes.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * VIEWPORT - */ -export const SET_VIEWPORT = 'VIEWPORT::SET'; -export const SET_VIEWPORT_ACTIVE = 'VIEWPORT::SET_ACTIVE'; -export const SET_VIEWPORT_LAYOUT = 'VIEWPORT::SET_LAYOUT'; -export const SET_VIEWPORT_LAYOUT_AND_DATA = - 'VIEWPORT::SET_VIEWPORT_LAYOUT_AND_DATA'; -export const CLEAR_VIEWPORT = 'VIEWPORT::CLEAR'; -export const SET_SPECIFIC_DATA = 'VIEWPORT::SET_SPECIFIC_DATA'; -export const SET_ACTIVE_SPECIFIC_DATA = 'VIEWPORT::SET_ACTIVE_SPECIFIC_DATA'; - -/** - * SERVERS - */ -export const ADD_SERVER = 'ADD_SERVER'; -export const SET_SERVERS = 'SET_SERVERS'; - -/** - * EXTENSIONS - */ -export const SET_EXTENSION_DATA = 'SET_EXTENSION_DATA'; - -/** - * PREFERENCES - * */ -export const SET_USER_PREFERENCES = 'SET_USER_PREFERENCES'; diff --git a/platform/core/src/redux/index.js b/platform/core/src/redux/index.js deleted file mode 100644 index aa41ca20e41..00000000000 --- a/platform/core/src/redux/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import actions from './actions.js'; -import reducers from './reducers'; -import localStorage from './localStorage.js'; -import sessionStorage from './sessionStorage.js'; - -const redux = { - reducers, - actions, - localStorage, - sessionStorage, -}; - -export default redux; diff --git a/platform/core/src/redux/index.test.js b/platform/core/src/redux/index.test.js deleted file mode 100644 index c2e16c28891..00000000000 --- a/platform/core/src/redux/index.test.js +++ /dev/null @@ -1,16 +0,0 @@ -import redux from './index.js'; - -describe('redux exports', () => { - test('have not changed', () => { - const expectedExports = [ - 'actions', - 'reducers', - 'localStorage', - 'sessionStorage', - ].sort(); - - const exports = Object.keys(redux).sort(); - - expect(exports).toEqual(expectedExports); - }); -}); diff --git a/platform/core/src/redux/localStorage.js b/platform/core/src/redux/localStorage.js deleted file mode 100644 index 96a2505c1de..00000000000 --- a/platform/core/src/redux/localStorage.js +++ /dev/null @@ -1,28 +0,0 @@ -const LocalStorageApi = window.localStorage; -const localStorageKey = 'state'; -export const loadState = () => { - try { - const serializedState = LocalStorageApi.getItem(localStorageKey); - if (!serializedState) { - return undefined; - } - - return JSON.parse(serializedState); - } catch (e) { - return undefined; - } -}; - -export const saveState = state => { - try { - const serializedState = JSON.stringify(state); - LocalStorageApi.setItem(localStorageKey, serializedState); - } catch (e) {} -}; - -const localStorage = { - saveState, - loadState, -}; - -export default localStorage; diff --git a/platform/core/src/redux/reducers/extensions.js b/platform/core/src/redux/reducers/extensions.js deleted file mode 100644 index 10b88b91361..00000000000 --- a/platform/core/src/redux/reducers/extensions.js +++ /dev/null @@ -1,25 +0,0 @@ -export const defaultState = {}; - -const extensions = (state = defaultState, action) => { - switch (action.type) { - case 'SET_EXTENSION_DATA': - const extensionName = action.extension; - const currentData = state[extensionName] || {}; - - const incomingData = action.data; - - const extension = { - [extensionName]: { - ...currentData, - ...incomingData, - }, - }; - - return { ...state, ...extension }; - - default: - return state; - } -}; - -export default extensions; diff --git a/platform/core/src/redux/reducers/extensions.test.js b/platform/core/src/redux/reducers/extensions.test.js deleted file mode 100644 index ff279795274..00000000000 --- a/platform/core/src/redux/reducers/extensions.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { Reducer } from 'redux-testkit'; - -import reducer, { defaultState } from './extensions'; -import * as actionTypes from './../constants/ActionTypes.js'; - -describe('viewports reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(defaultState); - }); - - it('should set new data on first SET_EXTENSION_DATA', () => { - const initialState = defaultState; - - const action = { - type: actionTypes.SET_EXTENSION_DATA, - extension: 'uber plugin', - data: { greeting: 'Hello!' }, - }; - - const expectedState = { - 'uber plugin': { greeting: 'Hello!' }, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should shallow-merge extension data, keeping unmodified fields, on SET_EXTENSION_DATA', () => { - const initialState = { - 'uber plugin': { greeting: 'Hello!', "Can't touch this": 42 }, - }; - - const action = { - type: actionTypes.SET_EXTENSION_DATA, - extension: 'uber plugin', - data: { greeting: 'Aloha!' }, - }; - - const expectedState = { - 'uber plugin': { greeting: 'Aloha!', "Can't touch this": 42 }, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); -}); diff --git a/platform/core/src/redux/reducers/index.js b/platform/core/src/redux/reducers/index.js deleted file mode 100644 index 44bf26755af..00000000000 --- a/platform/core/src/redux/reducers/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import extensions from './extensions'; -import loading from './loading'; -import preferences from './preferences'; -import servers from './servers'; -import studies from './studies'; -import timepointManager from './timepointManager'; -import viewports from './viewports'; - -const reducers = { - extensions, - loading, - preferences, - servers, - studies, - timepointManager, - viewports, -}; - -export default reducers; diff --git a/platform/core/src/redux/reducers/loading.js b/platform/core/src/redux/reducers/loading.js deleted file mode 100644 index b82a57d273c..00000000000 --- a/platform/core/src/redux/reducers/loading.js +++ /dev/null @@ -1,33 +0,0 @@ -import cloneDeep from 'lodash.clonedeep'; - -const defaultState = { - progress: {}, - lastUpdated: null, -}; - -const loading = (state = defaultState, action) => { - let progress; - let lastUpdated; - switch (action.type) { - case 'SET_STUDY_LOADING_PROGRESS': - progress = cloneDeep(state).progress; - progress[action.progressId] = action.progressData; - - // This is a workaround so we can easily identify changes - // to the progress object without doing deep comparison. - lastUpdated = new Date().getTime(); - - return Object.assign({}, state, { progress, lastUpdated }); - case 'CLEAR_STUDY_LOADING_PROGRESS': - progress = cloneDeep(state).progress; - delete progress[action.progressId]; - - lastUpdated = new Date().getTime(); - - return Object.assign({}, state, { progress, lastUpdated }); - default: - return state; - } -}; - -export default loading; diff --git a/platform/core/src/redux/reducers/preferences.js b/platform/core/src/redux/reducers/preferences.js deleted file mode 100644 index a9eca66e3d3..00000000000 --- a/platform/core/src/redux/reducers/preferences.js +++ /dev/null @@ -1,21 +0,0 @@ -import { windowLevelPresets } from '../../defaults'; - -const defaultState = { - windowLevelData: windowLevelPresets, - generalPreferences: { - // language: 'en-US' - }, -}; - -const preferences = (state = defaultState, action) => { - switch (action.type) { - case 'SET_USER_PREFERENCES': { - return Object.assign({}, state, action.state); - } - default: - return state; - } -}; - -export { defaultState }; -export default preferences; diff --git a/platform/core/src/redux/reducers/preferences.test.js b/platform/core/src/redux/reducers/preferences.test.js deleted file mode 100644 index fc81eaaac37..00000000000 --- a/platform/core/src/redux/reducers/preferences.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Reducer } from 'redux-testkit'; - -import reducer, { defaultState } from './preferences'; -import { SET_USER_PREFERENCES } from './../constants/ActionTypes.js'; - -describe('preferences reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(defaultState); - }); - - it('should set user preferences state and properly merge with current state', () => { - const initialState = defaultState; - - const action = { - type: SET_USER_PREFERENCES, - state: { generalPreferences: { language: 'es' } }, - }; - - const expectedState = { - windowLevelData: defaultState.windowLevelData, - generalPreferences: { - language: 'es', - }, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); -}); diff --git a/platform/core/src/redux/reducers/servers.js b/platform/core/src/redux/reducers/servers.js deleted file mode 100644 index 6b11b231064..00000000000 --- a/platform/core/src/redux/reducers/servers.js +++ /dev/null @@ -1,32 +0,0 @@ -import uniqBy from 'lodash/uniqBy'; - -export const defaultState = { - servers: [], -}; - -const servers = (state = defaultState, action) => { - switch (action.type) { - case 'ADD_SERVER': - let servers = uniqBy([...state.servers, action.server], 'id'); - servers.forEach(s => (s.active = true)); - return { ...state, servers }; - - case 'ACTIVATE_SERVER': { - const newServer = { ...action.server, active: true }; - const newServers = state.servers; - newServers.forEach(s => (s.active = false)); - return { - ...state, - servers: uniqBy([...newServers, newServer], 'wadoRoot'), - }; - } - - case 'SET_SERVERS': - return { ...state, servers: action.servers }; - - default: - return state; - } -}; - -export default servers; diff --git a/platform/core/src/redux/reducers/servers.test.js b/platform/core/src/redux/reducers/servers.test.js deleted file mode 100644 index cbd5155ed6c..00000000000 --- a/platform/core/src/redux/reducers/servers.test.js +++ /dev/null @@ -1,89 +0,0 @@ -import { Reducer } from 'redux-testkit'; - -import reducer, { defaultState } from './servers'; -import * as types from './../constants/ActionTypes.js'; - -describe('viewports reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(defaultState); - }); - - it('should result in one server when there were no servers before on ADD_SERVER', () => { - const initialState = defaultState; - - const action = { type: types.ADD_SERVER, server: { id: 'some-server-id' } }; - - const expectedState = { servers: [action.server] }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should add to servers list on ADD_SERVER', () => { - const initialState = { - servers: [ - { id: 'one', active: true }, - { id: 'two', active: true }, - { id: 'three', active: true }, - { id: 'four', active: true }, - ], - }; - - const action = { - type: types.ADD_SERVER, - server: { id: 'five', active: true }, - }; - - const expectedState = { - servers: [...initialState.servers, action.server], - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should not add duplicated servers on ADD_SERVER', () => { - const initialState = { - servers: [ - { id: 'one', active: true }, - { id: 'two', active: true }, - { id: 'three', active: true }, - { id: 'four', active: true }, - ], - }; - - const action = { - type: types.ADD_SERVER, - server: { id: 'two', active: true }, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(initialState); - }); - - it('should replace servers on SET_SERVERS', () => { - const initialState = { - servers: [{ id: 'one' }, { id: 'two' }, { id: 'three' }, { id: 'four' }], - }; - - const action = { - type: types.SET_SERVERS, - servers: [{ id: 'un' }, { id: 'deux' }, { id: 'trois' }], - }; - - const expectedState = { - servers: action.servers, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); -}); diff --git a/platform/core/src/redux/reducers/studies.js b/platform/core/src/redux/reducers/studies.js deleted file mode 100644 index b2545a28106..00000000000 --- a/platform/core/src/redux/reducers/studies.js +++ /dev/null @@ -1,19 +0,0 @@ -import cloneDeep from 'lodash.clonedeep'; - -const defaultState = { - studyData: {}, -}; - -const servers = (state = defaultState, action) => { - switch (action.type) { - case 'SET_STUDY_DATA': - const updatedStudyData = cloneDeep(state).studyData; - updatedStudyData[action.StudyInstanceUID] = action.data; - - return Object.assign({}, state, { studyData: updatedStudyData }); - default: - return state; - } -}; - -export default servers; diff --git a/platform/core/src/redux/reducers/timepointManager.js b/platform/core/src/redux/reducers/timepointManager.js deleted file mode 100644 index 98f03e8c4f7..00000000000 --- a/platform/core/src/redux/reducers/timepointManager.js +++ /dev/null @@ -1,17 +0,0 @@ -const defaultState = { - timepoints: [], - measurements: [], -}; - -const timepointManager = (state = defaultState, action) => { - switch (action.type) { - case 'SET_TIMEPOINTS': - return Object.assign({}, state, { timepoints: action.state }); - case 'SET_MEASUREMENTS': - return Object.assign({}, state, { measurements: action.state }); - default: - return state; - } -}; - -export default timepointManager; diff --git a/platform/core/src/redux/reducers/viewports.js b/platform/core/src/redux/reducers/viewports.js deleted file mode 100644 index 743d1900ded..00000000000 --- a/platform/core/src/redux/reducers/viewports.js +++ /dev/null @@ -1,240 +0,0 @@ -import cloneDeep from 'lodash.clonedeep'; -import produce, { setAutoFreeze } from 'immer'; - -import { - CLEAR_VIEWPORT, - SET_ACTIVE_SPECIFIC_DATA, - SET_SPECIFIC_DATA, - SET_VIEWPORT, - SET_VIEWPORT_ACTIVE, - SET_VIEWPORT_LAYOUT, - SET_VIEWPORT_LAYOUT_AND_DATA, -} from './../constants/ActionTypes.js'; - -setAutoFreeze(false); - -export const DEFAULT_STATE = { - numRows: 1, - numColumns: 1, - activeViewportIndex: 0, - layout: { - viewports: [{}], - }, - viewportSpecificData: {}, -}; - -/** - * Take the new number of Rows and Columns, delete all not used viewport data and also set - * active viewport as default in case current one is not available anymore. - * - * @param {Number} numRows - * @param {Number} numColumns - * @param {Object} currentViewportSpecificData - * @returns - */ -const findActiveViewportSpecificData = ( - numRows, - numColumns, - currentViewportSpecificData = {} -) => { - const numberOfViewports = numRows * numColumns; - const viewportSpecificData = cloneDeep(currentViewportSpecificData); - - if (numberOfViewports < Object.keys(viewportSpecificData).length) { - Object.keys(viewportSpecificData).forEach(key => { - if (key > numberOfViewports - 1) { - delete viewportSpecificData[key]; - } - }); - } - - return viewportSpecificData; -}; -/** - * Take new number of Rows and Columns and make sure the current active viewport index is still available, if not, return the default - * - * @param {Number} numRows - * @param {Number} numColumns - * @param {Number} currentActiveViewportIndex - * @returns - */ -const getActiveViewportIndex = ( - numRows, - numColumns, - currentActiveViewportIndex -) => { - const numberOfViewports = numRows * numColumns; - - return currentActiveViewportIndex > numberOfViewports - 1 - ? DEFAULT_STATE.activeViewportIndex - : currentActiveViewportIndex; -}; - -/** - * The definition of a viewport action. - * - * @typedef {Object} ViewportAction - * @property {string} type - - * @property {Object} data - - * @property {Object} layout - - * @property {number} viewportIndex - - * @property {Object} viewportSpecificData - - */ - -/** - * @param {Object} [state=DEFAULT_STATE] The current viewport state. - * @param {ViewportAction} action A viewport action. - */ -const viewports = (state = DEFAULT_STATE, action) => { - let useActiveViewport = false; - - switch (action.type) { - /** - * Sets the active viewport index. - * - * @return {Object} New state. - */ - case SET_VIEWPORT_ACTIVE: { - return produce(state, draftState => { - draftState.activeViewportIndex = getActiveViewportIndex( - draftState.numRows, - draftState.numColumns, - action.viewportIndex - ); - }); - } - - /** - * Sets viewport layout. - * - * @return {Object} New state. - */ - case SET_VIEWPORT_LAYOUT: { - const { numRows, numColumns } = action; - const viewportSpecificData = findActiveViewportSpecificData( - numRows, - numColumns, - state.viewportSpecificData - ); - const activeViewportIndex = getActiveViewportIndex( - numRows, - numColumns, - state.activeViewportIndex - ); - - return { - ...state, - numRows: action.numRows, - numColumns: action.numColumns, - layout: { viewports: [...action.viewports] }, - viewportSpecificData, - activeViewportIndex, - }; - } - - /** - * Sets viewport layout and data. - * - * @return {Object} New state. - */ - case SET_VIEWPORT_LAYOUT_AND_DATA: { - const { numRows, numColumns } = action; - const viewportSpecificData = findActiveViewportSpecificData( - numRows, - numColumns, - action.viewportSpecificData - ); - const activeViewportIndex = getActiveViewportIndex( - numRows, - numColumns, - state.activeViewportIndex - ); - - return { - ...state, - numRows: action.numRows, - numColumns: action.numColumns, - layout: { viewports: [...action.viewports] }, - viewportSpecificData, - activeViewportIndex, - }; - } - - /** - * Sets viewport specific data of active viewport. - * - * @return {Object} New state. - */ - case SET_VIEWPORT: { - return produce(state, draftState => { - draftState.viewportSpecificData[action.viewportIndex] = - draftState.viewportSpecificData[action.viewportIndex] || {}; - - Object.keys(action.viewportSpecificData).forEach(key => { - draftState.viewportSpecificData[action.viewportIndex][key] = - action.viewportSpecificData[key]; - }); - - if (action.viewportSpecificData && action.viewportSpecificData.plugin) { - draftState.layout.viewports[action.viewportIndex].plugin = - action.viewportSpecificData.plugin; - } - }); - } - - /** - * Sets viewport specific data of active/any viewport. - * - * @return {Object} New state. - */ - case SET_ACTIVE_SPECIFIC_DATA: - useActiveViewport = true; - // Allow fall-through - // eslint-disable-next-line - case SET_SPECIFIC_DATA: { - const layout = cloneDeep(state.layout); - const viewportIndex = useActiveViewport - ? state.activeViewportIndex - : action.viewportIndex; - - let viewportSpecificData = cloneDeep(state.viewportSpecificData); - viewportSpecificData[viewportIndex] = { - ...action.viewportSpecificData, - }; - - if (action.viewportSpecificData && action.viewportSpecificData.plugin) { - layout.viewports[viewportIndex].plugin = - action.viewportSpecificData.plugin; - } - - return { ...state, layout, viewportSpecificData }; - } - - /** - * Clears viewport specific data of any viewport. - * - * @return {Object} New state. - */ - case CLEAR_VIEWPORT: { - let viewportSpecificData = cloneDeep(state.viewportSpecificData); - - if (action.viewportIndex) { - viewportSpecificData[action.viewportIndex] = {}; - return { ...state, viewportSpecificData }; - } else { - return DEFAULT_STATE; - } - } - - /** - * Returns the current application state. - * - * @return {Object} The current state. - */ - default: { - return state; - } - } -}; - -export default viewports; diff --git a/platform/core/src/redux/reducers/viewports.test.js b/platform/core/src/redux/reducers/viewports.test.js deleted file mode 100644 index c7d3fe5a2b9..00000000000 --- a/platform/core/src/redux/reducers/viewports.test.js +++ /dev/null @@ -1,301 +0,0 @@ -import { Reducer } from 'redux-testkit'; - -import reducer, { DEFAULT_STATE } from './viewports.js'; -import * as types from './../constants/ActionTypes.js'; - -describe('viewports reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(DEFAULT_STATE); - }); - - it('should handle SET_VIEWPORT_ACTIVE with inexistent viewport index', () => { - const initialState = { - numRows: 4, - numColumns: 4, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT_ACTIVE, - viewportIndex: 100, - }; - - const expectedToChangeInState = { - activeViewportIndex: 0, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toChangeInState(expectedToChangeInState); - }); - - it('should handle SET_VIEWPORT_ACTIVE with existent viewport index', () => { - const initialState = { - numRows: 4, - numColumns: 4, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT_ACTIVE, - viewportIndex: 5, - }; - - const expectedToChangeInState = { - activeViewportIndex: 5, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toChangeInState(expectedToChangeInState); - }); - - it('should handle SET_VIEWPORT_LAYOUT', () => { - const initialState = DEFAULT_STATE; - - const action = { - type: types.SET_VIEWPORT_LAYOUT, - numRows: 1, - numColumns: 2, - viewports: [ - { - plugin: 'cornerstone', - }, - { - plugin: 'vtk', - }, - ], - }; - - const expectedToChangeInState = { - numRows: 1, - numColumns: 2, - layout: { - viewports: [ - { - plugin: 'cornerstone', - }, - { - plugin: 'vtk', - }, - ], - }, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toChangeInState(expectedToChangeInState); - }); - - it('should handle SET_VIEWPORT_LAYOUT when we reduce the number of viewports', () => { - const initialState = { - numRows: 1, - numColumns: 2, - viewportSpecificData: { - 0: { viewportData0: 'viewportData0' }, - 1: { viewportData1: 'viewportData1' }, - }, - layout: { - viewports: [], - }, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT_LAYOUT, - numRows: 1, - numColumns: 1, - viewports: [], - }; - - const expectedState = { - numRows: 1, - numColumns: 1, - viewportSpecificData: { - 0: { viewportData0: 'viewportData0' }, - }, - layout: { - viewports: [], - }, - activeViewportIndex: 0, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should handle SET_VIEWPORT_LAYOUT_AND_DATA', () => { - const initialState = { - numRows: 1, - numColumns: 1, - viewportSpecificData: { - 0: { viewportData0: 'data0' }, - }, - layout: { - viewports: [{ plugin: 'cornerstone' }], - }, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT_LAYOUT_AND_DATA, - numRows: 1, - numColumns: 2, - viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], - viewportSpecificData: { - 0: { viewportData0: 'NEWdata0' }, - 1: { viewportData1: 'NEWdata1' }, - }, - }; - - const expectedState = { - numRows: 1, - numColumns: 2, - viewportSpecificData: { - 0: { viewportData0: 'NEWdata0' }, - 1: { viewportData1: 'NEWdata1' }, - }, - layout: { - viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], - }, - activeViewportIndex: 0, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should handle SET_VIEWPORT_LAYOUT_AND_DATA when we reduce the number of viewports', () => { - const initialState = { - numRows: 1, - numColumns: 3, - viewportSpecificData: { - 0: { viewportData0: 'vtkData0' }, - 1: { viewportData1: 'vtkData1' }, - 2: { viewportData2: 'vtkData2' }, - }, - layout: { - viewports: [{ plugin: 'vtk' }, { plugin: 'vtk' }, { plugin: 'vtk' }], - }, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT_LAYOUT_AND_DATA, - numRows: 1, - numColumns: 1, - viewports: [{ plugin: 'cornerstone' }], - viewportSpecificData: { - 0: { viewportData0: 'cornerstoneData0' }, - }, - }; - - const expectedState = { - numRows: 1, - numColumns: 1, - viewportSpecificData: { - 0: { viewportData0: 'cornerstoneData0' }, - }, - layout: { - viewports: [{ plugin: 'cornerstone' }], - }, - activeViewportIndex: 0, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should handle SET_VIEWPORT when we only set one viewport specific data', () => { - const initialState = { - numRows: 1, - numColumns: 2, - viewportSpecificData: { - 0: { viewportData0: 'data0' }, - 1: { viewportData1: 'data1' }, - }, - layout: { - viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], - }, - activeViewportIndex: 0, - }; - - const action = { - type: types.SET_VIEWPORT, - viewportIndex: 1, - viewportSpecificData: { - viewportData1: 'NEWdata1', - }, - }; - - const expectedState = { - numRows: 1, - numColumns: 2, - viewportSpecificData: { - 0: { viewportData0: 'data0' }, - 1: { viewportData1: 'NEWdata1' }, - }, - layout: { - viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], - }, - activeViewportIndex: 0, - }; - - Reducer(reducer) - .withState(initialState) - .expect(action) - .toReturnState(expectedState); - }); - - it('should handle SET_VIEWPORT', () => { - const viewportToSet = 0; - const setViewportAction = { - type: types.SET_VIEWPORT, - viewportIndex: viewportToSet, - viewportSpecificData: { - hello: 'this is that data for the viewport', - world: 'that will be set for the viewportIndex', - }, - }; - - const updatedState = reducer(undefined, setViewportAction); - const updatedViewport = updatedState.viewportSpecificData[viewportToSet]; - - expect(updatedViewport).toEqual(setViewportAction.viewportSpecificData); - }); - - it('should handle CLEAR_VIEWPORT', () => { - const existingViewportData = { - viewportSpecificData: { - 0: { - viewportProperty: 'hello world', - }, - 1: { - viewportProperty: 'fizzbuzz', - }, - }, - }; - const clearViewportAction = { - type: types.CLEAR_VIEWPORT, - viewportIndex: 1, - }; - - const updatedState = reducer(existingViewportData, clearViewportAction); - const clearedViewport = - updatedState.viewportSpecificData[clearViewportAction.viewportIndex]; - const originalOtherViewport = existingViewportData.viewportSpecificData[0]; - const updatedOtherViewport = updatedState.viewportSpecificData[0]; - - expect(clearedViewport).toEqual({}); - expect(updatedOtherViewport).toEqual(originalOtherViewport); - }); -}); diff --git a/platform/core/src/redux/sessionStorage.js b/platform/core/src/redux/sessionStorage.js deleted file mode 100644 index 49360eafc95..00000000000 --- a/platform/core/src/redux/sessionStorage.js +++ /dev/null @@ -1,28 +0,0 @@ -const SessionStorageApi = window.sessionStorage; -const sessionStorageKey = 'state'; -export const loadState = () => { - try { - const serializedState = SessionStorageApi.getItem(sessionStorageKey); - if (!serializedState) { - return undefined; - } - - return JSON.parse(serializedState); - } catch (e) { - return undefined; - } -}; - -export const saveState = state => { - try { - const serializedState = JSON.stringify(state); - SessionStorageApi.setItem(sessionStorageKey, serializedState); - } catch (e) {} -}; - -const sessionStorage = { - saveState, - loadState, -}; - -export default sessionStorage; diff --git a/platform/core/src/studies/getStudyBoxData.js b/platform/core/src/studies/getStudyBoxData.js deleted file mode 100644 index 416838a0642..00000000000 --- a/platform/core/src/studies/getStudyBoxData.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Overridable namespace to allow getting study boxes data externally. - * - * The function must handle the first parameter as a studyInformation object containing at least the - * StudyInstanceUID attribute. - * - * Shall return a promise that will be resolved with an object containing those attributes: - * - StudyInstanceUID {String}: copy of studyInformation.StudyInstanceUID - * - modalities {String}: 2 uppercase letters for each Modality split by any non-alphabetical char(s) - * - StudyDate {String}: date formatted as YYYYMMDD - * - StudyDescription {String}: study description string - */ -// TODO: What is this for? -const getStudyBoxData = false; - -export default getStudyBoxData; diff --git a/platform/core/src/studies/retrieveStudiesMetadata.js b/platform/core/src/studies/retrieveStudiesMetadata.js deleted file mode 100644 index ebed831cbdc..00000000000 --- a/platform/core/src/studies/retrieveStudiesMetadata.js +++ /dev/null @@ -1,40 +0,0 @@ -import log from '../log.js'; -import { retrieveStudyMetadata } from './retrieveStudyMetadata'; - -/** - * Retrieves metaData for multiple studies at once. - * - * This function calls retrieveStudyMetadata several times, asynchronously, - * and waits for all of the results to be returned. - * - * @param {Object} server Object with server configuration parameters - * @param {Array} studyInstanceUIDs The UIDs of the Studies to be retrieved - * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @returns {Promise} that will be resolved with the metadata or rejected with the error - */ -export default function retrieveStudiesMetadata( - server, - studyInstanceUIDs, - filters -) { - // Create an empty array to store the Promises for each metaData retrieval call - const promises = []; - - // Loop through the array of studyInstanceUIDs - studyInstanceUIDs.forEach(function(StudyInstanceUID) { - // Send the call and resolve or reject the related promise based on its outcome - const promise = retrieveStudyMetadata(server, StudyInstanceUID, filters); - - // Add the current promise to the array of promises - promises.push(promise); - }); - - // When all of the promises are complete, this callback runs - const promise = Promise.all(promises); - - // Warn the error on console if some retrieval failed - promise.catch(error => log.warn(error)); - - return promise; -} diff --git a/platform/core/src/studies/retrieveStudyMetadata.js b/platform/core/src/studies/retrieveStudyMetadata.js deleted file mode 100644 index b2d84be9ef5..00000000000 --- a/platform/core/src/studies/retrieveStudyMetadata.js +++ /dev/null @@ -1,59 +0,0 @@ -import RetrieveMetadata from './services/wado/retrieveMetadata.js'; - -const moduleName = 'RetrieveStudyMetadata'; -// Cache for promises. Prevents unnecessary subsequent calls to the server -const StudyMetaDataPromises = new Map(); - -/** - * Retrieves study metadata - * - * @param {Object} server Object with server configuration parameters - * @param {string} StudyInstanceUID The UID of the Study to be retrieved - * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @returns {Promise} that will be resolved with the metadata or rejected with the error - */ -export function retrieveStudyMetadata(server, StudyInstanceUID, filters) { - // @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all - // and further requests for that metadata will always fail. On failure, we probably need to remove the - // corresponding promise from the "StudyMetaDataPromises" map... - - if (!server) { - throw new Error(`${moduleName}: Required 'server' parameter not provided.`); - } - if (!StudyInstanceUID) { - throw new Error( - `${moduleName}: Required 'StudyInstanceUID' parameter not provided.` - ); - } - - // Already waiting on result? Return cached promise - if (StudyMetaDataPromises.has(StudyInstanceUID)) { - return StudyMetaDataPromises.get(StudyInstanceUID); - } - - // Create a promise to handle the data retrieval - const promise = new Promise((resolve, reject) => { - RetrieveMetadata(server, StudyInstanceUID, filters).then(function(data) { - resolve(data); - }, reject); - }); - - // Store the promise in cache - StudyMetaDataPromises.set(StudyInstanceUID, promise); - - return promise; -} - -/** - * Delete the cached study metadata retrieval promise to ensure that the browser will - * re-retrieve the study metadata when it is next requested - * - * @param {String} StudyInstanceUID The UID of the Study to be removed from cache - * - */ -export function deleteStudyMetadataPromise(StudyInstanceUID) { - if (StudyMetaDataPromises.has(StudyInstanceUID)) { - StudyMetaDataPromises.delete(StudyInstanceUID); - } -} diff --git a/platform/core/src/studies/retrieveStudyMetadata.test.js b/platform/core/src/studies/retrieveStudyMetadata.test.js deleted file mode 100644 index 4725ab562d4..00000000000 --- a/platform/core/src/studies/retrieveStudyMetadata.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import { retrieveStudyMetadata } from './retrieveStudyMetadata.js'; - -const fakeDicomWebServer = {}; - -// Testing Promises: https://jestjs.io/docs/en/asynchronous#promises -describe('retrieveStudyMetadata.js', () => { - it('throws an exception if no server parameter is provided', () => { - const callWithNoServer = () => { - retrieveStudyMetadata(null, 'fake-study-instance-uid'); - }; - - expect(callWithNoServer).toThrow(Error); - }); - - it('throws an exception if no StudyInstanceUID parameter is provided', () => { - const callWithNoStudyInstanceUID = () => { - retrieveStudyMetadata(fakeDicomWebServer, null); - }; - - expect(callWithNoStudyInstanceUID).toThrow(Error); - }); - - it('caches and returns the same promise for identical studyInstanceUIDs', () => { - const firstPromise = retrieveStudyMetadata( - fakeDicomWebServer, - 'fake-study-instance-uid' - ); - const secondPromise = retrieveStudyMetadata( - fakeDicomWebServer, - 'fake-study-instance-uid' - ); - - expect(firstPromise).toBe(secondPromise); - }); -}); diff --git a/platform/core/src/studies/searchStudies.js b/platform/core/src/studies/searchStudies.js deleted file mode 100644 index e2169ded8fc..00000000000 --- a/platform/core/src/studies/searchStudies.js +++ /dev/null @@ -1,26 +0,0 @@ -import Studies from './services/qido/studies'; - -const studySearchPromises = new Map(); - -/** - * Search for studies information by the given filter - * - * @param {Object} filter Filter that will be used on search - * @returns {Promise} resolved with an array of studies information or rejected with an error - */ -export default function searchStudies(server, filter) { - const promiseKeyObj = { - qidoRoot: server.qidoRoot, - filter, - }; - const promiseKey = JSON.stringify(promiseKeyObj); - if (studySearchPromises.has(promiseKey)) { - return studySearchPromises.get(promiseKey); - } else { - const promise = Studies(server, filter); - - studySearchPromises.set(promiseKey, promise); - - return promise; - } -} diff --git a/platform/core/src/studies/services/index.js b/platform/core/src/studies/services/index.js deleted file mode 100644 index c059fefc75a..00000000000 --- a/platform/core/src/studies/services/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// DICOMWeb instance, study, and metadata retrieval -import Instances from './qido/instances.js'; -import Studies from './qido/studies.js'; -import RetrieveMetadata from './wado/retrieveMetadata.js'; - -const WADO = { - RetrieveMetadata, -}; - -const QIDO = { - Studies, - Instances, -}; - -export { QIDO, WADO }; diff --git a/platform/core/src/studies/services/qido/instances.js b/platform/core/src/studies/services/qido/instances.js deleted file mode 100644 index 5ecf99d34d0..00000000000 --- a/platform/core/src/studies/services/qido/instances.js +++ /dev/null @@ -1,102 +0,0 @@ -import DICOMWeb from '../../../DICOMWeb/'; -import { api } from 'dicomweb-client'; - -import errorHandler from '../../../errorHandler'; - -/** - * Parses data returned from a QIDO search and transforms it into - * an array of series that are present in the study - * - * @param server The DICOM server - * @param StudyInstanceUID - * @param resultData - * @returns {Array} Series List - */ -function resultDataToStudyMetadata(server, StudyInstanceUID, resultData) { - const seriesMap = {}; - const series = []; - - resultData.forEach(function(instance) { - // Use seriesMap to cache series data - // If the series instance UID has already been used to - // process series data, continue using that series - var SeriesInstanceUID = DICOMWeb.getString(instance['0020000E']); - var series = seriesMap[SeriesInstanceUID]; - - // If no series data exists in the seriesMap cache variable, - // process any available series data - if (!series) { - series = { - SeriesInstanceUID: SeriesInstanceUID, - SeriesNumber: DICOMWeb.getString(instance['00200011']), - instances: [], - }; - - // Save this data in the seriesMap cache variable - seriesMap[SeriesInstanceUID] = series; - series.push(series); - } - - // The uri for the dicomweb - // NOTE: DCM4CHEE seems to return the data zipped - // NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't - // know how to parse yet - //var uri = DICOMWeb.getString(instance['00081190']); - //uri = uri.replace('wado-rs', 'dicom-web'); - - // manually create a WADO-URI from the UIDs - // NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured? - var SOPInstanceUID = DICOMWeb.getString(instance['00080018']); - var uri = - server.wadoUriRoot + - '?requestType=WADO&studyUID=' + - StudyInstanceUID + - '&seriesUID=' + - SeriesInstanceUID + - '&objectUID=' + - SOPInstanceUID + - '&contentType=application%2Fdicom'; - - // Add this instance to the current series - series.instances.push({ - SOPClassUID: DICOMWeb.getString(instance['00080016']), - SOPInstanceUID: SOPInstanceUID, - uri: uri, - InstanceNumber: DICOMWeb.getString(instance['00200013']), - }); - }); - return series; -} - -/** - * Retrieve a set of instances using a QIDO call - * @param server - * @param StudyInstanceUID - * @throws ECONNREFUSED - * @returns {{wadoUriRoot: String, StudyInstanceUID: String, series: Array}} - */ -export default function Instances(server, StudyInstanceUID) { - // TODO: Are we using this function anywhere?? Can we remove it? - - const config = { - url: server.qidoRoot, - headers: DICOMWeb.getAuthorizationHeader(server), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }; - const dicomWeb = new api.DICOMwebClient(config); - const queryParams = getQIDOQueryParams( - filter, - server.qidoSupportsIncludeField - ); - const options = { - studyInstanceUID: StudyInstanceUID, - }; - - return dicomWeb.searchForInstances(options).then(result => { - return { - wadoUriRoot: server.wadoUriRoot, - StudyInstanceUID: StudyInstanceUID, - series: resultDataToStudyMetadata(server, StudyInstanceUID, result.data), - }; - }); -} diff --git a/platform/core/src/studies/services/qido/studies.js b/platform/core/src/studies/services/qido/studies.js deleted file mode 100644 index ddf5b0fd3a6..00000000000 --- a/platform/core/src/studies/services/qido/studies.js +++ /dev/null @@ -1,84 +0,0 @@ -import { api } from 'dicomweb-client'; -import DICOMWeb from '../../../DICOMWeb/'; - -import errorHandler from '../../../errorHandler'; - -/** - * Creates a QIDO date string for a date range query - * Assumes the year is positive, at most 4 digits long. - * - * @param date The Date object to be formatted - * @returns {string} The formatted date string - */ -function dateToString(date) { - if (!date) return ''; - let year = date.getFullYear().toString(); - let month = (date.getMonth() + 1).toString(); - let day = date.getDate().toString(); - year = '0'.repeat(4 - year.length).concat(year); - month = '0'.repeat(2 - month.length).concat(month); - day = '0'.repeat(2 - day.length).concat(day); - return ''.concat(year, month, day); -} - -/** - * Parses resulting data from a QIDO call into a set of Study MetaData - * - * @param resultData - * @returns {Array} An array of Study MetaData objects - */ -function resultDataToStudies(resultData) { - const studies = []; - - if (!resultData || !resultData.length) return; - - resultData.forEach(study => - studies.push({ - StudyInstanceUID: DICOMWeb.getString(study['0020000D']), - // 00080005 = SpecificCharacterSet - StudyDate: DICOMWeb.getString(study['00080020']), - StudyTime: DICOMWeb.getString(study['00080030']), - AccessionNumber: DICOMWeb.getString(study['00080050']), - referringPhysicianName: DICOMWeb.getString(study['00080090']), - // 00081190 = URL - PatientName: DICOMWeb.getName(study['00100010']), - PatientID: DICOMWeb.getString(study['00100020']), - PatientBirthdate: DICOMWeb.getString(study['00100030']), - patientSex: DICOMWeb.getString(study['00100040']), - studyId: DICOMWeb.getString(study['00200010']), - numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']), - numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']), - StudyDescription: DICOMWeb.getString(study['00081030']), - // Modality: DICOMWeb.getString(study['00080060']), - // ModalitiesInStudy: DICOMWeb.getString(study['00080061']), - modalities: DICOMWeb.getString( - DICOMWeb.getModalities(study['00080060'], study['00080061']) - ), - }) - ); - - return studies; -} - -export default function Studies(server, filter) { - const config = { - url: server.qidoRoot, - headers: DICOMWeb.getAuthorizationHeader(server), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }; - - const dicomWeb = new api.DICOMwebClient(config); - server.qidoSupportsIncludeField = - server.qidoSupportsIncludeField === undefined - ? true - : server.qidoSupportsIncludeField; - const queryParams = getQIDOQueryParams( - filter, - server.qidoSupportsIncludeField - ); - const options = { - queryParams, - }; - - return dicomWeb.searchForStudies(options).then(resultDataToStudies); -} diff --git a/platform/core/src/studies/services/wado/getReferencedSeriesSequence.js b/platform/core/src/studies/services/wado/getReferencedSeriesSequence.js deleted file mode 100644 index ef32b16fde7..00000000000 --- a/platform/core/src/studies/services/wado/getReferencedSeriesSequence.js +++ /dev/null @@ -1,45 +0,0 @@ -import DICOMWeb from '../../../DICOMWeb'; - -/** - * Function to get series sequence (sequence of pepeating items where each - * item includes the attributes of one or more series) based on a given sopInstance. - * - * @param {Object} instance The sop instance - * @returns {Promise} Referenced series sequence - */ -const getReferencedSeriesSequence = instance => { - const referencedSeriesSequenceRaw = instance['00081115']; - - const referencedSeriesSequence = []; - - if (referencedSeriesSequenceRaw && referencedSeriesSequenceRaw.Value) { - referencedSeriesSequenceRaw.Value.forEach(referencedSeries => { - const referencedSeriesInstanceUID = DICOMWeb.getString( - referencedSeries['0020000E'] - ); - - const referencedInstanceSequenceRaw = referencedSeries['0008114A']; - const referencedInstanceSequence = []; - - referencedInstanceSequenceRaw.Value.forEach(referencedInstance => { - referencedInstanceSequence.push({ - referencedSOPClassUID: DICOMWeb.getString( - referencedInstance['00081150'] - ), - referencedSOPInstanceUID: DICOMWeb.getString( - referencedInstance['00081155'] - ), - }); - }); - - referencedSeriesSequence.push({ - referencedSeriesInstanceUID, - referencedInstanceSequence, - }); - }); - } - - return referencedSeriesSequence; -}; - -export default getReferencedSeriesSequence; diff --git a/platform/core/src/studies/services/wado/retrieveMetadata.js b/platform/core/src/studies/services/wado/retrieveMetadata.js deleted file mode 100644 index f3c544450ef..00000000000 --- a/platform/core/src/studies/services/wado/retrieveMetadata.js +++ /dev/null @@ -1,30 +0,0 @@ -import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync'; -import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync'; - -/** - * Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series - * will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed. - * - * @param {Object} server Object with server configuration parameters - * @param {string} StudyInstanceUID The Study Instance UID of the study which needs to be loaded - * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @returns {Object} A study descriptor object - */ -async function RetrieveMetadata(server, StudyInstanceUID, filters = {}) { - const RetrieveMetadataLoader = - server.enableStudyLazyLoad != false - ? RetrieveMetadataLoaderAsync - : RetrieveMetadataLoaderSync; - - const retrieveMetadataLoader = new RetrieveMetadataLoader( - server, - StudyInstanceUID, - filters - ); - const studyMetadata = retrieveMetadataLoader.execLoad(); - - return studyMetadata; -} - -export default RetrieveMetadata; diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoader.js b/platform/core/src/studies/services/wado/retrieveMetadataLoader.js deleted file mode 100644 index 2786f4cdf97..00000000000 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoader.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Class to define inheritance of load retrieve strategy. - * The process can be async load (lazy) or sync load - * - * There are methods that must be implemented at consumer level - * To retrieve study call execLoad - */ -export default class RetrieveMetadataLoader { - /** - * @constructor - * @param {Object} server Object with server configuration parameters - * @param {Array} studyInstanceUID Study instance ui to be retrieved - * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - */ - constructor(server, studyInstanceUID, filters = {}) { - this.server = server; - this.studyInstanceUID = studyInstanceUID; - this.filters = filters; - } - - async execLoad() { - await this.configLoad(); - const preLoadData = await this.preLoad(); - const loadData = await this.load(preLoadData); - const postLoadData = await this.posLoad(loadData); - - return postLoadData; - } - - /** - * It iterates over given loaders running each one. Loaders parameters must be bind when getting it. - * @param {Array} loaders - array of loader to retrieve data. - */ - async runLoaders(loaders) { - let result; - for (const loader of loaders) { - try { - result = await loader(); - if (result && result.length) { - break; // closes iterator in case data is retrieved successfully - } - } catch (e) { - throw e; - } - } - - if (loaders.next().done && !result) { - throw new Error('RetrieveMetadataLoader failed'); - } - - return result; - } - - // Methods to be overwrite - async configLoad() {} - async preLoad() {} - async load(preLoadData) {} - async posLoad(loadData) {} -} diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js deleted file mode 100644 index 90d1ae98842..00000000000 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js +++ /dev/null @@ -1,79 +0,0 @@ -import { api } from 'dicomweb-client'; -import DICOMWeb from '../../../DICOMWeb/'; -import { createStudyFromSOPInstanceList } from './studyInstanceHelpers'; -import RetrieveMetadataLoader from './retrieveMetadataLoader'; - -import errorHandler from '../../../errorHandler'; - -/** - * Class for sync load of study metadata. - * It inherits from RetrieveMetadataLoader - * - * A list of loaders (getLoaders) can be created so, it will be applied a fallback load strategy. - * I.e Retrieve metadata using all loaders possibilities. - */ -export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader { - getOptions() { - const { studyInstanceUID, filters } = this; - - const options = { - studyInstanceUID, - }; - - const { seriesInstanceUID } = filters; - if (seriesInstanceUID) { - options['seriesInstanceUID'] = seriesInstanceUID; - } - - return options; - } - - /** - * @returns {Array} Array of loaders. To be consumed as queue - */ - *getLoaders() { - const loaders = []; - const { - studyInstanceUID, - filters: { seriesInstanceUID } = {}, - client, - } = this; - - if (seriesInstanceUID) { - loaders.push( - client.retrieveSeriesMetadata.bind(client, { - studyInstanceUID, - seriesInstanceUID, - }) - ); - } - - loaders.push( - client.retrieveStudyMetadata.bind(client, { studyInstanceUID }) - ); - - yield* loaders; - } - - configLoad() { - const { server } = this; - const client = new api.DICOMwebClient({ - url: server.wadoRoot, - headers: DICOMWeb.getAuthorizationHeader(server), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }); - - this.client = client; - } - - async load(preLoadData) { - const loaders = this.getLoaders(); - const result = this.runLoaders(loaders); - return result; - } - - async posLoad(loadData) { - const { server } = this; - return createStudyFromSOPInstanceList(server, loadData); - } -} diff --git a/platform/core/src/studies/services/wado/studyInstanceHelpers.js b/platform/core/src/studies/services/wado/studyInstanceHelpers.js deleted file mode 100644 index bc99eaba4df..00000000000 --- a/platform/core/src/studies/services/wado/studyInstanceHelpers.js +++ /dev/null @@ -1,211 +0,0 @@ -import DICOMWeb from '../../../DICOMWeb'; -import metadataProvider from '../../../classes/MetadataProvider'; -import getWADORSImageId from '../../../utils/getWADORSImageId'; -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; -import getReferencedSeriesSequence from './getReferencedSeriesSequence'; - -/** - * Create a plain JS object that describes a study (a study descriptor object) - * @param {Object} server Object with server configuration parameters - * @param {Object} aSopInstance a SOP Instance from which study information will be added - */ -function createStudy(server, aSopInstance) { - // TODO: Pass a reference ID to the server instead of including the URLs here - return { - series: [], - seriesMap: Object.create(null), - seriesLoader: null, - wadoUriRoot: server.wadoUriRoot, - wadoRoot: server.wadoRoot, - qidoRoot: server.qidoRoot, - PatientName: DICOMWeb.getName(aSopInstance['00100010']), - PatientID: DICOMWeb.getString(aSopInstance['00100020']), - PatientAge: DICOMWeb.getNumber(aSopInstance['00101010']), - PatientSize: DICOMWeb.getNumber(aSopInstance['00101020']), - PatientWeight: DICOMWeb.getNumber(aSopInstance['00101030']), - AccessionNumber: DICOMWeb.getString(aSopInstance['00080050']), - StudyTime: DICOMWeb.getString(aSopInstance['00080030']), - StudyDate: DICOMWeb.getString(aSopInstance['00080020']), - FrameOfReferenceUID: DICOMWeb.getString(aSopInstance['00200052']), - ReferencedSeriesSequence: getReferencedSeriesSequence(aSopInstance), - modalities: DICOMWeb.getString(aSopInstance['00080061']), // TODO -> Rename this.. it'll take a while to not mess this one up. - StudyDescription: DICOMWeb.getString(aSopInstance['00081030']), - NumberOfStudyRelatedInstances: DICOMWeb.getString(aSopInstance['00201208']), - StudyInstanceUID: DICOMWeb.getString(aSopInstance['0020000D']), - InstitutionName: DICOMWeb.getString(aSopInstance['00080080']), - }; -} - -/** Returns a WADO url for an instance - * - * @param StudyInstanceUID - * @param SeriesInstanceUID - * @param SOPInstanceUID - * @returns {string} - */ -function buildInstanceWadoUrl( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID -) { - // TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though - const params = []; - - params.push('requestType=WADO'); - params.push(`studyUID=${StudyInstanceUID}`); - params.push(`seriesUID=${SeriesInstanceUID}`); - params.push(`objectUID=${SOPInstanceUID}`); - params.push('contentType=application/dicom'); - params.push('transferSyntax=*'); - - const paramString = params.join('&'); - - return `${server.wadoUriRoot}?${paramString}`; -} - -function buildInstanceWadoRsUri( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID -) { - return `${server.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}`; -} - -function buildInstanceFrameWadoRsUri( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - frame -) { - const baseWadoRsUri = buildInstanceWadoRsUri( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID - ); - frame = frame != null || 1; - - return `${baseWadoRsUri}/frames/${frame}`; -} - -async function makeSOPInstance(server, study, instance) { - const naturalizedInstance = await metadataProvider.addInstance(instance, { - server, - }); - - const { - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - } = naturalizedInstance; - - let series = study.seriesMap[SeriesInstanceUID]; - - if (!series) { - series = { - SeriesInstanceUID, - SeriesDescription: naturalizedInstance.SeriesDescription, - Modality: naturalizedInstance.Modality, - SeriesNumber: naturalizedInstance.SeriesNumber, - SeriesDate: naturalizedInstance.SeriesDate, - SeriesTime: naturalizedInstance.SeriesTime, - instances: [], - }; - study.seriesMap[SeriesInstanceUID] = series; - study.series.push(series); - } - - const wadouri = buildInstanceWadoUrl( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID - ); - const baseWadoRsUri = buildInstanceWadoRsUri( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID - ); - const wadorsuri = buildInstanceFrameWadoRsUri( - server, - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID - ); - - const sopInstance = { - metadata: naturalizedInstance, - baseWadoRsUri, - wadouri, - wadorsuri, - wadoRoot: server.wadoRoot, - imageRendering: server.imageRendering, - thumbnailRendering: server.thumbnailRendering, - }; - - series.instances.push(sopInstance); - - if ( - sopInstance.thumbnailRendering === 'wadors' || - sopInstance.imageRendering === 'wadors' - ) { - // If using WADO-RS for either images or thumbnails, - // Need to add this to cornerstoneWADOImageLoader's provider - // (it won't be hit on cornerstone.metaData.get, but cornerstoneWADOImageLoader - // will cry if you don't add data to cornerstoneWADOImageLoader.wadors.metaDataManager). - - const wadoRSMetadata = Object.assign(instance); - - const { NumberOfFrames } = sopInstance.metadata; - - if (NumberOfFrames) { - for (let i = 0; i < NumberOfFrames; i++) { - const wadorsImageId = getWADORSImageId(sopInstance, i); - - cornerstoneWADOImageLoader.wadors.metaDataManager.add( - wadorsImageId, - wadoRSMetadata - ); - } - } else { - const wadorsImageId = getWADORSImageId(sopInstance); - - cornerstoneWADOImageLoader.wadors.metaDataManager.add( - wadorsImageId, - wadoRSMetadata - ); - } - } - - return sopInstance; -} - -/** - * Add a list of SOP Instances to a given study object descriptor - * @param {Object} server Object with server configuration parameters - * @param {Object} study The study descriptor to which the given SOP instances will be added - * @param {Array} sopInstanceList A list of SOP instance objects - */ -async function addInstancesToStudy(server, study, sopInstanceList) { - return Promise.all( - sopInstanceList.map(function(sopInstance) { - return makeSOPInstance(server, study, sopInstance); - }) - ); -} - -const createStudyFromSOPInstanceList = async (server, sopInstanceList) => { - if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) { - const firstSopInstance = sopInstanceList[0]; - const study = createStudy(server, firstSopInstance); - await addInstancesToStudy(server, study, sopInstanceList); - return study; - } - throw new Error('Failed to create study out of provided SOP instance list'); -}; - -export { createStudyFromSOPInstanceList, addInstancesToStudy }; diff --git a/platform/core/src/ui/getOffset.js b/platform/core/src/ui/getOffset.js deleted file mode 100644 index 773d7525399..00000000000 --- a/platform/core/src/ui/getOffset.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Get the offset for the given element - * - * @param {Object} element DOM element which will have the offser calculated - * @returns {Object} Object containing the top and left offset - */ -export default function getOffset(element) { - let top = 0; - let left = 0; - if (element.offsetParent) { - do { - left += element.offsetLeft; - top += element.offsetTop; - } while ((element = element.offsetParent)); - } - - return { - left, - top, - }; -} diff --git a/platform/core/src/ui/getScrollbarSize.js b/platform/core/src/ui/getScrollbarSize.js deleted file mode 100644 index 16362c638dd..00000000000 --- a/platform/core/src/ui/getScrollbarSize.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Get the vertical and horizontal scrollbar sizes - * Got from https://stackoverflow.com/questions/986937/how-can-i-get-the-browsers-scrollbar-sizes - * - * @returns {Array} Array containing the scrollbar horizontal and vertical sizes - */ -export default function getScrollbarSize() { - const inner = document.createElement('p'); - inner.style.width = '100%'; - inner.style.height = '100%'; - - const outer = document.createElement('div'); - outer.style.position = 'absolute'; - outer.style.top = '0px'; - outer.style.left = '0px'; - outer.style.visibility = 'hidden'; - outer.style.width = '100px'; - outer.style.height = '100px'; - outer.style.overflow = 'hidden'; - outer.appendChild(inner); - - document.body.appendChild(outer); - - const w1 = inner.offsetWidth; - const h1 = inner.offsetHeight; - outer.style.overflow = 'scroll'; - let w2 = inner.offsetWidth; - let h2 = inner.offsetHeight; - - if (w1 === w2) { - w2 = outer.clientWidth; - } - - if (h1 === h2) { - h2 = outer.clientHeight; - } - - document.body.removeChild(outer); - - return [w1 - w2, h1 - h2]; -} diff --git a/platform/core/src/ui/handleError.js b/platform/core/src/ui/handleError.js deleted file mode 100644 index cbfb6f27c2f..00000000000 --- a/platform/core/src/ui/handleError.js +++ /dev/null @@ -1,33 +0,0 @@ -import log from '../log.js'; - -export default function handleError(error) { - let { title, message } = error; - - if (!title) { - if (error instanceof Error) { - title = error.name; - } - } - - if (!message) { - if (error instanceof Error) { - message = error.message; - } - } - - const data = Object.assign( - { - title, - message, - class: 'themed', - hideConfirm: true, - cancelLabel: 'Dismiss', - cancelClass: 'btn-secondary', - }, - error || {} - ); - - log.error(error); - // TODO: Find a better way to handle errors instead of displaying a dialog for all of them. - // OHIF.ui.showDialog('dialogForm', data); -} diff --git a/platform/core/src/ui/index.js b/platform/core/src/ui/index.js deleted file mode 100644 index 942807a6675..00000000000 --- a/platform/core/src/ui/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import unsavedChanges from './unsavedChanges.js'; -import handleError from './handleError.js'; -import isCharacterKeyPress from './isCharacterKeyPress.js'; -import getOffset from './getOffset.js'; -import getScrollbarSize from './getScrollbarSize.js'; - -const ui = { - getScrollbarSize, - getOffset, - isCharacterKeyPress, - handleError, -}; - -export default ui; diff --git a/platform/core/src/ui/isCharacterKeyPress.js b/platform/core/src/ui/isCharacterKeyPress.js deleted file mode 100644 index b3c5b91e9c3..00000000000 --- a/platform/core/src/ui/isCharacterKeyPress.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Check if the pressed key combination will result in a character input - * Got from https://stackoverflow.com/questions/4179708/how-to-detect-if-the-pressed-key-will-produce-a-character-inside-an-input-text - * - * @returns {Boolean} Whether the pressed key combination will input a character or not - */ -export default function isCharacterKeyPress(event) { - if (typeof event.which === 'undefined') { - // This is IE, which only fires keypress events for printable keys - return true; - } else if (typeof event.which === 'number' && event.which > 0) { - // In other browsers except old versions of WebKit, event.which is - // only greater than zero if the keypress is a printable key. - // We need to filter out backspace and ctrl/alt/meta key combinations - return ( - !event.ctrlKey && !event.metaKey && !event.altKey && event.which !== 8 - ); - } - - return false; -} diff --git a/platform/core/src/ui/unsavedChanges.js b/platform/core/src/ui/unsavedChanges.js deleted file mode 100644 index cbc00f9ed6c..00000000000 --- a/platform/core/src/ui/unsavedChanges.js +++ /dev/null @@ -1,536 +0,0 @@ -const FUNCTION = 'function'; -const STRING = 'string'; -const UNDEFINED = 'undefined'; -const WILDCARD = '*'; // "*" is a special name which means "all children". -const SEPARATOR = '.'; - -/** - * Main Namespace Component Class - */ - -class Node { - constructor() { - this.value = 0; - this.children = {}; - this.handlers = {}; - } - - getPathComponents(path) { - return typeof path === STRING ? path.split(SEPARATOR) : null; - } - - getNodeUpToIndex(path, index) { - let node = this; - - for (let i = 0; i < index; ++i) { - let item = path[i]; - if (node.children.hasOwnProperty(item)) { - node = node.children[item]; - } else { - node = null; - break; - } - } - - return node; - } - - append(name, value) { - const children = this.children; - let node = null; - - if (children.hasOwnProperty(name)) { - node = children[name]; - } else if (typeof name === STRING && name !== WILDCARD) { - node = new Node(); - children[name] = node; - } - - if (node !== null) { - node.value += value > 0 ? parseInt(value) : 0; - } - - return node; - } - - probe(recursively) { - let value = this.value; - - // Calculate entire tree value recursively? - if (recursively === true) { - const children = this.children; - for (let item in children) { - if (children.hasOwnProperty(item)) { - value += children[item].probe(recursively); - } - } - } - - return value; - } - - clear(recursively) { - this.value = 0; - - // Clear entire tree recursively? - if (recursively === true) { - const children = this.children; - for (let item in children) { - if (children.hasOwnProperty(item)) { - children[item].clear(recursively); - } - } - } - } - - appendPath(path, value) { - path = this.getPathComponents(path); - - if (path !== null) { - const last = path.length - 1; - let node = this; - for (let i = 0; i < last; ++i) { - node = node.append(path[i], 0); - if (node === null) { - return false; - } - } - - return node.append(path[last], value) !== null; - } - - return false; - } - - clearPath(path, recursively) { - path = this.getPathComponents(path); - - if (path !== null) { - const last = path.length - 1; - let node = this.getNodeUpToIndex(path, last); - if (node !== null) { - let item = path[last]; - if (item !== WILDCARD) { - if (node.children.hasOwnProperty(item)) { - node.children[item].clear(recursively); - return true; - } - } else { - const children = node.children; - for (item in children) { - if (children.hasOwnProperty(item)) { - children[item].clear(recursively); - } - } - - return true; - } - } - } - - return false; - } - - probePath(path, recursively) { - path = this.getPathComponents(path); - - if (path !== null) { - const last = path.length - 1; - let node = this.getNodeUpToIndex(path, last); - if (node !== null) { - let item = path[last]; - if (item !== WILDCARD) { - if (node.children.hasOwnProperty(item)) { - return node.children[item].probe(recursively); - } - } else { - const children = node.children; - let value = 0; - for (item in children) { - if (children.hasOwnProperty(item)) { - value += children[item].probe(recursively); - } - } - - return value; - } - } - } - - return 0; - } - - attachHandler(type, handler) { - let result = false; - - if (typeof type === STRING && typeof handler === FUNCTION) { - const handlers = this.handlers; - const list = handlers.hasOwnProperty(type) - ? handlers[type] - : (handlers[type] = []); - const length = list.length; - - let notFound = true; - - for (let i = 0; i < length; ++i) { - if (handler === list[i]) { - notFound = false; - break; - } - } - - if (notFound) { - list[length] = handler; - result = true; - } - } - - return result; - } - - removeHandler(type, handler) { - let result = false; - - if (typeof type === STRING && typeof handler === FUNCTION) { - const handlers = this.handlers; - if (handlers.hasOwnProperty(type)) { - const list = handlers[type]; - const length = list.length; - for (let i = 0; i < length; ++i) { - if (handler === list[i]) { - list.splice(i, 1); - result = true; - break; - } - } - } - } - - return result; - } - - trigger(type, nonRecursively) { - if (typeof type === STRING) { - const handlers = this.handlers; - - if (handlers.hasOwnProperty(type)) { - const list = handlers[type]; - const length = list.length; - for (let i = 0; i < length; ++i) { - list[i].call(null); - } - } - - if (nonRecursively !== true) { - const children = this.children; - for (let item in children) { - if (children.hasOwnProperty(item)) { - children[item].trigger(type); - } - } - } - } - } - - attachHandlerForPath(path, type, handler) { - path = this.getPathComponents(path); - - if (path !== null) { - let node = this.getNodeUpToIndex(path, path.length); - if (node !== null) { - return node.attachHandler(type, handler); - } - } - - return false; - } - - removeHandlerForPath(path, type, handler) { - path = this.getPathComponents(path); - - if (path !== null) { - let node = this.getNodeUpToIndex(path, path.length); - if (node !== null) { - return node.removeHandler(type, handler); - } - } - - return false; - } - - triggerHandlersForPath(path, type, nonRecursively) { - path = this.getPathComponents(path); - - if (path !== null) { - let node = this.getNodeUpToIndex(path, path.length); - if (node !== null) { - node.trigger(type, nonRecursively); - } - } - } -} - -/** - * Root Namespace Node and API - */ - -const rootNode = new Node(); - -export const unsavedChanges = { - rootNode: rootNode, - - observer: null, //new Tracker.Dependency(), - - hooks: new Map(), - - /** - * Register a reactive dependency on every change any path suffers - */ - depend: function() { - return; // this.observer.depend(); - }, - - /** - * Signal an unsaved change for a given namespace. - * @param {String} path A string (e.g., "viewer.studyViewer.measurements.targets") that identifies the namespace of the signaled changes. - * @return {Boolean} Returns false if the signal could not be saved or the supplied namespace is invalid. Otherwise, true is returned. - */ - set: function(path) { - const result = rootNode.appendPath(path, 1); - //this.observer.changed(); - return result; - }, - - /** - * Clear all signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that namespace - * are cleared. - * @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" - * for clearing the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" to specify all signaled - * changes for the "viewer.studyViewer" namespace). - * @param {Boolean} recursively Clear node and all its children recursively. If not specified defaults to true. - * @return {Boolean} Returns false if the signal could not be removed or the supplied namespace is invalid. Otherwise, true is returned. - */ - clear: function(path, recursively) { - const result = rootNode.clearPath( - path, - typeof recursively === UNDEFINED ? true : recursively - ); - //this.observer.changed(); - return result; - }, - - /** - * Count the amount of signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that - * namespace will also be accounted. - * @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" - * for counting the amount of signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" - * to count all signaled changes for the "viewer.studyViewer" namespace). - * @param {Boolean} recursively Probe node and all its children recursively. If not specified defaults to true. - * @return {Number} Returns the amount of signaled changes for a given namespace. If the supplied namespace is a wildcard, the sum of all - * changes for that namespace are returned. - */ - probe: function(path, recursively) { - return rootNode.probePath( - path, - typeof recursively === UNDEFINED ? true : recursively - ); - }, - - /** - * Attach an event handler to the specified namespace. - * @param {String} name A string that identifies the namespace to which the event handler will be attached (e.g., - * "viewer.studyViewer.measurements" to attach an event handler for that namespace). - * @param {String} type A string that identifies the event type to which the event handler will be attached. - * @param {Function} handler The handler that will be executed when the specifed event is triggered. - * @return {Boolean} Returns true on success and false on failure. - */ - attachHandler: function(path, type, handler) { - return ( - rootNode.appendPath(path, 0) && - rootNode.attachHandlerForPath(path, type, handler) - ); - }, - - /** - * Detach an event handler from the specified namespace. - * @param {String} name A string that identifies the namespace from which the event handler will be detached (e.g., - * "viewer.studyViewer.measurements" to remove an event handler from that namespace). - * @param {String} type A string that identifies the event type to which the event handler was attached. - * @param {Function} handler The handler that will be removed from execution list. - * @return {Boolean} Returns true on success and false on failure. - */ - removeHandler: function(path, type, handler) { - return rootNode.removeHandlerForPath(path, type, handler); - }, - - /** - * Trigger all event handlers for the specified namespace and type. - * @param {String} name A string that identifies the namespace from which the event handler will be detached (e.g., - * "viewer.studyViewer.measurements" to remove an event handler from that namespace). - * @param {String} type A string that identifies the event type which will be triggered. - * @param {Boolean} nonRecursively If set to true, prevents triggering event handlers from descending tree. - * @return {Void} No value is returned. - */ - trigger: function(path, type, nonRecursively) { - rootNode.triggerHandlersForPath(path, type, nonRecursively); - }, - - /** - * UI utility that presents a confirmation dialog to the user if any unsaved changes where signaled for the given namespace. - * @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" - * for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" - * to consider all signaled changes for the "viewer.studyViewer" namespace). - * @param {Function} callback A callback function (e.g, function(shouldProceed, hasChanges) { ... }) that will be executed after assessment. - * Upon execution, the callback will receive two boolean arguments (shouldProceed and hasChanges) indicating if the action can be performed - * or not and if changes that need to be cleared exist. - * @param {Object} options (Optional) An object with UI presentation options. - * @param {String} options.title The string that will be used as a title for confirmation dialog. - * @param {String} options.message The string that will be used as a message for confirmation dialog. - * @return {void} No value is returned. - */ - checkBeforeAction: function(path, callback, options) { - let probe, hasChanges, shouldProceed; - - if (typeof callback !== 'function') { - // nothing to do if no callback function is supplied... - return; - } - - probe = this.probe(path); - if (probe > 0) { - // Unsaved changes exist... - hasChanges = true; - let dialogOptions = Object.assign( - { - title: 'You have unsaved changes!', - message: - "Your changes will be lost if you don't save them before leaving the current page... Are you sure you want to proceed?", - }, - options - ); - OHIF.ui.showDialog('dialogConfirm', dialogOptions).then( - function() { - // Unsaved changes exist but user confirms action... - shouldProceed = true; - callback.call(null, shouldProceed, hasChanges); - }, - function() { - // Unsaved changes exist and user does NOT confirm action... - shouldProceed = false; - callback.call(null, shouldProceed, hasChanges); - } - ); - } else { - // No unsaved changes, action can be performed... - hasChanges = false; - shouldProceed = true; - callback.call(null, shouldProceed, hasChanges); - } - }, - - /** - * UI utility that presents a "proactive" dialog (with three options: stay, abandon-changes, save-changes) to the user if any unsaved changes where signaled for the given namespace. - * @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" - * for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" - * to consider all signaled changes for the "viewer.studyViewer" namespace). - * @param {Function} callback A callback function (e.g, function(hasChanges, userChoice) { ... }) that will be executed after assessment. - * Upon execution, the callback will receive two arguments: one boolean (hasChanges) indicating that unsaved changes exist and one string with the ID of the - * option picked by the user on the dialog ('abort-action', 'abandon-changes' and 'save-changes'). If no unsaved changes exist, the second argument is null. - * @param {Object} options (Optional) An object with UI presentation options. - * @param {Object} options.position An object with optimal position (e.g., { x: ..., y: ... }) for the dialog. - * @return {void} No value is returned. - */ - presentProactiveDialog: function(path, callback, options) { - let probe, hasChanges; - - if (typeof callback !== 'function') { - // nothing to do if no callback function is supplied... - return; - } - - probe = this.probe(path, true); - if (probe > 0) { - // Unsaved changes exist... - hasChanges = true; - OHIF.ui.unsavedChangesDialog(function(choice) { - callback.call(null, hasChanges, choice); - }, options); - } else { - // No unsaved changes, action can be performed... - hasChanges = false; - callback.call(null, hasChanges, null); - } - }, - - addHook(saveCallback, options = {}) { - if (!options.path) { - options.path = '*'; - } - - if (!options.message) { - options.message = 'There are unsaved changes'; - } - - this.hooks.set(saveCallback, options); - }, - - removeHook(saveCallback) { - this.hooks.delete(saveCallback); - }, - - confirmNavigation(navigateCallback, event) { - let dialogPresented = false; - Array.from(this.hooks.keys()).every(saveCallback => { - const options = this.hooks.get(saveCallback); - const probe = this.probe(options.path, true); - if (!probe) return true; - - const dialogOptions = Object.assign({ class: 'themed' }, options); - if (event) { - dialogOptions.position = { - x: event.clientX + 15, - y: event.clientY + 15, - }; - } - - OHIF.ui.unsavedChanges.presentProactiveDialog( - options.path, - (hasChanges, userChoice) => { - if (!hasChanges) return; - - const clear = () => this.clear(options.path, true); - switch (userChoice) { - case 'abort-action': - return; - case 'save-changes': - const result = saveCallback(); - if (result instanceof Promise) { - return result.then(() => { - clear(); - this.confirmNavigation(navigateCallback, event); - }); - } - - clear(); - return this.confirmNavigation(navigateCallback, event); - case 'abandon-changes': - clear(); - break; - } - - navigateCallback(); - }, - dialogOptions - ); - - dialogPresented = true; - return false; - }); - - if (!dialogPresented) { - navigateCallback(); - } - }, -}; - -export default unsavedChanges; diff --git a/platform/core/src/utils/StackManager.js b/platform/core/src/utils/StackManager.js index aed017a45d2..237f8697c87 100644 --- a/platform/core/src/utils/StackManager.js +++ b/platform/core/src/utils/StackManager.js @@ -1,6 +1,4 @@ import OHIFError from '../classes/OHIFError.js'; -import getImageId from './getImageId'; -import metadataProvider from '../classes/MetadataProvider.js'; let stackMap = {}; let configuration = {}; diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 4fa21a67e21..b4eb711e8df 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -5,11 +5,10 @@ import addServers from './addServers'; import guid from './guid'; import sortBy from './sortBy.js'; import sortBySeriesDate from './sortBySeriesDate.js'; -import studyMetadataManager from './studyMetadataManager'; import writeScript from './writeScript.js'; import DicomLoaderService from './dicomLoaderService.js'; import b64toBlob from './b64toBlob.js'; -import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js'; +//import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js'; import * as urlUtil from './urlUtil'; import makeDeferred from './makeDeferred'; import makeCancelable from './makeCancelable'; @@ -23,6 +22,10 @@ import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; import isLowPriorityModality from './isLowPriorityModality'; +// Commented out unused functionality. +// Now that we have the DicomMetadataStore, we no longer need the studyMetadataManager +// Need to implement new mechanism for dervived displaySets using the displaySetManager. + const utils = { guid, ObjectPath, @@ -35,10 +38,10 @@ const utils = { formatPN, b64toBlob, StackManager, - studyMetadataManager, + //studyMetadataManager, DicomLoaderService, urlUtil, - loadAndCacheDerivedDisplaySets, + //loadAndCacheDerivedDisplaySets, makeDeferred, makeCancelable, hotkeys, @@ -60,10 +63,9 @@ export { writeScript, b64toBlob, StackManager, - studyMetadataManager, DicomLoaderService, urlUtil, - loadAndCacheDerivedDisplaySets, + //loadAndCacheDerivedDisplaySets, makeDeferred, makeCancelable, hotkeys, diff --git a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js b/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js deleted file mode 100644 index 24a30418e10..00000000000 --- a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js +++ /dev/null @@ -1,111 +0,0 @@ -import studyMetadataManager from './studyMetadataManager'; - -/** - * Study schema - * - * @typedef {Object} Study - * @property {Array} seriesList - - * @property {Object} seriesMap - - * @property {Object} seriesLoader - - * @property {string} wadoUriRoot - - * @property {string} wadoRoot - - * @property {string} qidoRoot - - * @property {string} patientName - - * @property {string} patientId - - * @property {number} patientAge - - * @property {number} patientSize - - * @property {number} patientWeight - - * @property {string} accessionNumber - - * @property {string} studyDate - - * @property {string} studyTime - - * @property {string} modalities - - * @property {string} studyDescription - - * @property {string} imageCount - - * @property {string} studyInstanceUid - - * @property {string} institutionName - - * @property {Array} displaySets - - */ - -/** - * Factory function to load and cache derived display sets. - * - * @param {object} referencedDisplaySet Display set - * @param {string} referencedDisplaySet.displaySetInstanceUID Display set instance uid - * @param {string} referencedDisplaySet.seriesDate - * @param {string} referencedDisplaySet.seriesTime - * @param {string} referencedDisplaySet.seriesInstanceUid - * @param {string} referencedDisplaySet.seriesNumber - * @param {string} referencedDisplaySet.seriesDescription - * @param {number} referencedDisplaySet.numImageFrames - * @param {string} referencedDisplaySet.frameRate - * @param {string} referencedDisplaySet.modality - * @param {boolean} referencedDisplaySet.isMultiFrame - * @param {number} referencedDisplaySet.instanceNumber - * @param {boolean} referencedDisplaySet.isReconstructable - * @param {string} referencedDisplaySet.studyInstanceUid - * @param {Array} referencedDisplaySet.sopClassUids - * @param {Study[]} studies Collection of studies - * @returns void - */ -const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => { - const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet; - - const promises = []; - - const studyMetadata = studyMetadataManager.get(StudyInstanceUID); - - if (!studyMetadata) { - return promises; - } - - const derivedDisplaySets = studyMetadata.getDerivedDatasets({ - referencedSeriesInstanceUID: SeriesInstanceUID, - }); - - if (!derivedDisplaySets.length) { - return promises; - } - - // Filter by type - const displaySetsPerModality = {}; - - derivedDisplaySets.forEach(displaySet => { - const Modality = displaySet.Modality; - - if (displaySetsPerModality[Modality] === undefined) { - displaySetsPerModality[Modality] = []; - } - - displaySetsPerModality[Modality].push(displaySet); - }); - - // For each type, see if any are loaded, if not load the most recent. - Object.keys(displaySetsPerModality).forEach(key => { - const displaySets = displaySetsPerModality[key]; - const isLoaded = displaySets.some(displaySet => displaySet.isLoaded); - - if (isLoaded) { - return; - } - - // find most recent and load it. - let recentDateTime = 0; - let recentDisplaySet = displaySets[0]; - - displaySets.forEach(displaySet => { - const dateTime = Number( - `${displaySet.SeriesDate}${displaySet.SeriesTime}` - ); - if (dateTime > recentDateTime) { - recentDateTime = dateTime; - recentDisplaySet = displaySet; - } - }); - - promises.push(recentDisplaySet.load(referencedDisplaySet, studies)); - }); - - return promises; -}; - -export default loadAndCacheDerivedDisplaySets; diff --git a/platform/core/src/utils/studyMetadataManager.js b/platform/core/src/utils/studyMetadataManager.js deleted file mode 100644 index 79da423263e..00000000000 --- a/platform/core/src/utils/studyMetadataManager.js +++ /dev/null @@ -1,31 +0,0 @@ -import { TypeSafeCollection } from '../classes/TypeSafeCollection'; - -const studyMetadataList = new TypeSafeCollection(); - -function add(studyMetadata) { - studyMetadataList.insert(studyMetadata); -} - -function get(studyInstanceUID) { - return studyMetadataList.findBy({ studyInstanceUID }); -} - -function all(options) { - return studyMetadataList.all(options); -} - -function remove(studyInstanceUID) { - studyMetadataList.remove({ studyInstanceUID }); -} - -function purge() { - studyMetadataList.removeAll(); -} - -export default { - add, - get, - all, - remove, - purge, -}; diff --git a/platform/viewer/src/config.js b/platform/viewer/src/config.js deleted file mode 100644 index 8a56e7cd71f..00000000000 --- a/platform/viewer/src/config.js +++ /dev/null @@ -1,61 +0,0 @@ -import OHIF from '@ohif/core'; -import cornerstone from 'cornerstone-core'; -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; -import dicomParser from 'dicom-parser'; -import version from './version.js'; -import AppContext from './context/AppContext'; - -export function setConfiguration(appConfig) { - let homepage; - const { process } = window; - if (process && process.env && process.env.PUBLIC_URL) { - homepage = process.env.PUBLIC_URL; - } - - window.info = { - version, - homepage, - }; - - // For debugging - //if (process.env.node_env === 'development') { - window.cornerstone = cornerstone; - window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader; - //} - - cornerstoneWADOImageLoader.external.cornerstone = cornerstone; - cornerstoneWADOImageLoader.external.dicomParser = dicomParser; - - OHIF.user.getAccessToken = () => { - // TODO: Get the Redux store from somewhere else - const state = window.store.getState(); - if (!state.oidc || !state.oidc.user) { - return; - } - - return state.oidc.user.access_token; - }; - - OHIF.errorHandler.getHTTPErrorHandler = () => { - // const { appConfig = {} } = AppContext; - - return appConfig.httpErrorHandler; - }; - - cornerstoneWADOImageLoader.configure({ - beforeSend: function(xhr) { - const headers = OHIF.DICOMWeb.getAuthorizationHeader(); - - if (headers.Authorization) { - xhr.setRequestHeader('Authorization', headers.Authorization); - } - }, - errorInterceptor: error => { - // const { appConfig = {} } = AppContext; - - if (typeof appConfig.httpErrorHandler === 'function') { - appConfig.httpErrorHandler(error); - } - }, - }); -} diff --git a/platform/viewer/src/googleCloud/DatasetPicker.js b/platform/viewer/src/googleCloud/DatasetPicker.js deleted file mode 100644 index 120f021c0c9..00000000000 --- a/platform/viewer/src/googleCloud/DatasetPicker.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import api from './api/GoogleCloudApi'; -import DatasetsList from './DatasetsList'; -import './googleCloud.css'; - -export default class DatasetPicker extends Component { - state = { - error: null, - loading: true, - datasets: [], - filterStr: '', - }; - - static propTypes = { - project: PropTypes.object, - location: PropTypes.object, - onSelect: PropTypes.func, - accessToken: PropTypes.string, - }; - - async componentDidMount() { - api.setAccessToken(this.props.accessToken); - - const response = await api.loadDatasets( - this.props.project.projectId, - this.props.location.locationId - ); - - if (response.isError) { - this.setState({ - error: response.message, - }); - - return; - } - - this.setState({ - datasets: response.data.datasets || [], - loading: false, - }); - } - - render() { - const { datasets, loading, error, filterStr } = this.state; - const { onSelect } = this.props; - return ( -
- this.setState({ filterStr: e.target.value })} - /> - -
- ); - } -} diff --git a/platform/viewer/src/googleCloud/DatasetSelector.js b/platform/viewer/src/googleCloud/DatasetSelector.js deleted file mode 100644 index f1179c32b26..00000000000 --- a/platform/viewer/src/googleCloud/DatasetSelector.js +++ /dev/null @@ -1,155 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; -import DicomStorePicker from './DicomStorePicker'; -import DatasetPicker from './DatasetPicker'; -import ProjectPicker from './ProjectPicker'; -import LocationPicker from './LocationPicker'; -import GoogleCloudApi from './api/GoogleCloudApi'; -import './googleCloud.css'; - -class DatasetSelector extends Component { - state = { - project: null, - location: null, - dataset: null, - unloading: false, - }; - - static propTypes = { - id: PropTypes.string, - event: PropTypes.string, - user: PropTypes.object, - canClose: PropTypes.string, - setServers: PropTypes.func.isRequired, - }; - - onProjectSelect = project => { - this.setState({ - project, - }); - }; - - onLocationSelect = location => { - this.setState({ - location, - }); - }; - - onDatasetSelect = dataset => { - this.setState({ - dataset, - }); - }; - - onProjectClick = () => { - this.setState({ - dataset: null, - location: null, - project: null, - }); - }; - - onLocationClick = () => { - this.setState({ - dataset: null, - location: null, - }); - }; - - onDatasetClick = () => { - this.setState({ - dataset: null, - }); - }; - - onDicomStoreSelect = dicomStoreJson => { - const dicomStore = dicomStoreJson.name; - const parts = dicomStore.split('/'); - const result = { - wadoUriRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`, - qidoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`, - wadoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`, - project: parts[1], - location: parts[3], - dataset: parts[5], - dicomStore: parts[7], - }; - this.props.setServers(result); - }; - - render() { - const accessToken = this.props.user.access_token; - - const { project, location, dataset } = this.state; - const { - onProjectClick, - onLocationClick, - onDatasetClick, - onProjectSelect, - onLocationSelect, - onDatasetSelect, - onDicomStoreSelect, - } = this; - - let projectBreadcrumbs = ( -
- {this.props.t('Select a Project')} -
- ); - - if (project) { - projectBreadcrumbs = ( -
- {project.name} - {project && location && ( - - {' '} - -> {location.name.split('/')[3]} - - )} - {project && location && dataset && ( - - {' '} - -> {dataset.name.split('/')[5]} - - )} -
- ); - } - - return ( - <> - {projectBreadcrumbs} - {!project && ( - - )} - - {project && !location && ( - - )} - {project && location && !dataset && ( - - )} - {project && location && dataset && ( - - )} - - ); - } -} - -export default withTranslation('Common')(DatasetSelector); diff --git a/platform/viewer/src/googleCloud/DicomStorePicker.js b/platform/viewer/src/googleCloud/DicomStorePicker.js deleted file mode 100644 index 7944d1b93f8..00000000000 --- a/platform/viewer/src/googleCloud/DicomStorePicker.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import api from './api/GoogleCloudApi'; -import DicomStoreList from './DicomStoreList'; -import './googleCloud.css'; - -export default class DicomStorePicker extends Component { - state = { - error: null, - loading: true, - stores: [], - locations: [], - filterStr: '', - }; - - static propTypes = { - dataset: PropTypes.object, - onSelect: PropTypes.func, - accessToken: PropTypes.string.isRequired, - }; - - async componentDidMount() { - api.setAccessToken(this.props.accessToken); - - const response = await api.loadDicomStores(this.props.dataset.name); - - if (response.isError) { - this.setState({ - error: response.message, - }); - - return; - } - - this.setState({ - stores: response.data.dicomStores || [], - loading: false, - }); - } - - render() { - const { stores, loading, error, filterStr } = this.state; - const { onSelect } = this.props; - - return ( -
- this.setState({ filterStr: e.target.value })} - /> - -
- ); - } -} diff --git a/platform/viewer/src/googleCloud/DicomStorePickerModal.js b/platform/viewer/src/googleCloud/DicomStorePickerModal.js deleted file mode 100644 index eaa3e7ae1f4..00000000000 --- a/platform/viewer/src/googleCloud/DicomStorePickerModal.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import DatasetSelector from './DatasetSelector'; -import './googleCloud.css'; -import { withTranslation } from 'react-i18next'; -import * as GoogleCloudUtilServers from './utils/getServers'; - -import { servicesManager } from './../App.jsx'; - -function DicomStorePickerModal({ - isOpen = false, - setServers, - onClose, - user, - url, - t, -}) { - const { UIModalService } = servicesManager.services; - - const showDicomStorePickerModal = () => { - const handleEvent = data => { - const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore); - setServers(servers); - - // Force auto close - UIModalService.hide(); - onClose(); - }; - - if (UIModalService) { - UIModalService.show({ - content: DatasetSelector, - title: t('Google Cloud Healthcare API'), - contentProps: { - setServers: handleEvent, - user, - url, - }, - }); - } - }; - - return ( - {isOpen && showDicomStorePickerModal()} - ); -} - -DicomStorePickerModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - setServers: PropTypes.func.isRequired, - onClose: PropTypes.func, - user: PropTypes.object.isRequired, - url: PropTypes.string, -}; - -export default withTranslation('Common')(DicomStorePickerModal); diff --git a/platform/viewer/src/googleCloud/LocationPicker.js b/platform/viewer/src/googleCloud/LocationPicker.js deleted file mode 100644 index 0766d5c5f7a..00000000000 --- a/platform/viewer/src/googleCloud/LocationPicker.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import api from './api/GoogleCloudApi'; -import LocationsList from './LocationsList'; -import './googleCloud.css'; - -export default class LocationPicker extends Component { - state = { - error: null, - loading: true, - locations: [], - filterStr: '', - }; - - static propTypes = { - project: PropTypes.object, - onSelect: PropTypes.func, - accessToken: PropTypes.string, - }; - - async componentDidMount() { - api.setAccessToken(this.props.accessToken); - - const response = await api.loadLocations(this.props.project.projectId); - - if (response.isError) { - this.setState({ - error: response.message, - }); - - return; - } - - this.setState({ - locations: response.data.locations || [], - loading: false, - }); - } - - render() { - const { locations, loading, error, filterStr } = this.state; - const { onSelect } = this.props; - return ( -
- this.setState({ filterStr: e.target.value })} - /> - -
- ); - } -} diff --git a/platform/viewer/src/googleCloud/ProjectPicker.js b/platform/viewer/src/googleCloud/ProjectPicker.js deleted file mode 100644 index 080b970842a..00000000000 --- a/platform/viewer/src/googleCloud/ProjectPicker.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import api from './api/GoogleCloudApi'; -import ProjectsList from './ProjectsList'; -import './googleCloud.css'; - -export default class ProjectPicker extends Component { - state = { - error: null, - loading: true, - projects: [], - }; - - static propTypes = { - onSelect: PropTypes.func, - accessToken: PropTypes.string, - }; - - async componentDidMount() { - api.setAccessToken(this.props.accessToken); - const response = await api.loadProjects(); - - if (response.isError) { - this.setState({ - error: response.message, - }); - - return; - } - - this.setState({ - projects: response.data.projects || [], - filterStr: '', - loading: false, - }); - } - - render() { - const { projects, loading, filterStr, error } = this.state; - const { onSelect } = this.props; - return ( -
- this.setState({ filterStr: e.target.value })} - /> - -
- ); - } -} diff --git a/platform/viewer/src/googleCloud/api/DicomUploadService.js b/platform/viewer/src/googleCloud/api/DicomUploadService.js deleted file mode 100644 index d5b85c8390c..00000000000 --- a/platform/viewer/src/googleCloud/api/DicomUploadService.js +++ /dev/null @@ -1,89 +0,0 @@ -import { httpErrorToStr, checkDicomFile } from '../utils/helpers'; -import { api } from 'dicomweb-client'; -import { errorHandler } from '@ohif/core'; - -class DicomUploadService { - async smartUpload(files, url, uploadCallback, cancellationToken) { - const CHUNK_SIZE = 1; // Only one file per request is supported so far - const MAX_PARALLEL_JOBS = 50; // FIXME: tune MAX_PARALLEL_JOBS number - - let filesArray = Array.from(files); - if (filesArray.length === 0) { - throw new Error('No files were provided.'); - } - - let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS); - let completed = false; - - const processJob = async (resolve, reject) => { - while (filesArray.length > 0) { - if (cancellationToken.get()) return; - let chunk = filesArray.slice(0, CHUNK_SIZE); - filesArray = filesArray.slice(CHUNK_SIZE); - let error = null; - try { - if (chunk.length > 1) throw new Error('Not implemented'); - if (chunk.length === 1) await this.simpleUpload(chunk[0], url); - } catch (err) { - // It looks like a stupid bug of Babel that err is not an actual Exception object - error = httpErrorToStr(err); - } - chunk.forEach(file => uploadCallback(file.fileId, error)); - if (!completed && filesArray.length === 0) { - completed = true; - resolve(); - return; - } - } - }; - - await new Promise(resolve => { - for (let i = 0; i < parallelJobsCount; i++) { - processJob(resolve); - } - }); - } - - async simpleUpload(file, url) { - const client = this.getClient(url); - const loadedFile = await this.readFile(file); - const content = loadedFile.content; - if (!checkDicomFile(content)) - throw new Error('This is not a valid DICOM file.'); - - await client.storeInstances({ datasets: [content] }); - } - - readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - resolve({ - name: file.name, - size: file.size, - type: file.type, - content: reader.result, - }); - }; - reader.onerror = error => reject(error); - reader.readAsArrayBuffer(file); - }); - } - - setRetrieveAuthHeaderFunction(func) { - this.retrieveAuthHeaderFunc = func; - } - - getClient(url) { - const headers = this.retrieveAuthHeaderFunc(); - const errorInterceptor = errorHandler.getHTTPErrorHandler(); - - // TODO: a bit weird we are creating a new dicomweb client instance for every upload - return new api.DICOMwebClient({ - url, - headers, - }); - } -} - -export default new DicomUploadService(); diff --git a/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js b/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js deleted file mode 100644 index d485ca55eaa..00000000000 --- a/platform/viewer/src/lib/localFileLoaders/dicomFileLoader.js +++ /dev/null @@ -1,99 +0,0 @@ -import dcmjs from 'dcmjs'; -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; -import FileLoader from './fileLoader'; -import OHIF from '@ohif/core'; - -const metadataProvider = OHIF.cornerstone.metadataProvider; - -const DICOMFileLoader = new (class extends FileLoader { - fileType = 'application/dicom'; - loadFile(file, imageId) { - return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId); - } - - getDataset(image, imageId) { - let dataset = {}; - try { - const dicomData = dcmjs.data.DicomMessage.readFile(image); - - dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( - dicomData.dict - ); - - metadataProvider.addInstance(dataset); - - dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset( - dicomData.meta - ); - } catch (e) { - console.error('Error reading dicom file', e); - } - // Set imageId on dataset to be consumed later on - dataset.imageId = imageId; - - return dataset; - } - - getStudies(dataset, imageId) { - return this.getStudyFromDataset(dataset); - } - - getStudyFromDataset(dataset = {}) { - const { - StudyInstanceUID, - StudyDate, - StudyTime, - AccessionNumber, - ReferringPhysicianName, - PatientName, - PatientID, - PatientBirthDate, - PatientSex, - StudyID, - StudyDescription, - SeriesInstanceUID, - SeriesDescription, - SeriesNumber, - imageId, - } = dataset; - - const instance = { - metadata: dataset, - url: imageId, - }; - - const series = { - SeriesInstanceUID: SeriesInstanceUID, - SeriesDescription: SeriesDescription, - SeriesNumber: SeriesNumber, - instances: [instance], - }; - - const study = { - StudyInstanceUID, - StudyDate, - StudyTime, - AccessionNumber, - ReferringPhysicianName, - PatientName, - PatientID, - PatientBirthDate, - PatientSex, - StudyID, - StudyDescription, - /* - TODO: in case necessary to uncomment this block, double check every property - numberOfStudyRelatedSeries: NumberOfStudyRelatedSeries || DICOMWeb.getString(dataset['00201206']), - numberOfStudyRelatedInstances: NumberOfStudyRelatedInstances || DICOMWeb.getString(dataset['00201208']), - Modality: Modality || DICOMWeb.getString(dataset['00080060']), - ModalitiesInStudy: ModalitiesInStudy || DICOMWeb.getString(dataset['00080061']), - modalities: - */ - series: [series], - }; - - return study; - } -})(); - -export default DICOMFileLoader; diff --git a/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js b/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js deleted file mode 100644 index 9a2bbe534ae..00000000000 --- a/platform/viewer/src/lib/localFileLoaders/fileLoaderService.js +++ /dev/null @@ -1,82 +0,0 @@ -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; -import FileLoader from './fileLoader'; -import PDFFileLoader from './pdfFileLoader'; -import DICOMFileLoader from './dicomFileLoader'; - -class FileLoaderService extends FileLoader { - fileType; - loader; - constructor(file) { - super(); - const fileType = file && file.type; - this.loader = this.getLoader(fileType); - this.fileType = this.loader.fileType; - } - - static groupSeries(studies) { - const groupBy = (list, groupByKey, listKey) => { - let nonKeyCounter = 1; - - return list.reduce((acc, obj) => { - let key = obj[groupByKey]; - const list = obj[listKey]; - - // in case key not found, group it using counter - key = !!key ? key : '' + nonKeyCounter++; - - if (!acc[key]) { - acc[key] = { ...obj }; - acc[key][listKey] = []; - } - - acc[key][listKey].push(...list); - - return acc; - }, {}); - }; - - const studiesGrouped = Object.values( - groupBy(studies, 'StudyInstanceUID', 'series') - ); - - const result = studiesGrouped.map(studyGroup => { - const seriesGrouped = groupBy( - studyGroup.series, - 'SeriesInstanceUID', - 'instances' - ); - studyGroup.series = Object.values(seriesGrouped); - - return studyGroup; - }); - - return result; - } - - addFile(file) { - return cornerstoneWADOImageLoader.wadouri.fileManager.add(file); - } - - loadFile(file, imageId) { - return this.loader.loadFile(file, imageId); - } - - getDataset(image, imageId) { - return this.loader.getDataset(image, imageId); - } - - getStudies(dataset, imageId) { - return this.loader.getStudies(dataset, imageId); - } - - getLoader(fileType) { - if (fileType === 'application/pdf') { - return PDFFileLoader; - } else { - // Default to dicom loader - return DICOMFileLoader; - } - } -} - -export default FileLoaderService; diff --git a/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js b/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js deleted file mode 100644 index bf2ae6ec2a2..00000000000 --- a/platform/viewer/src/lib/localFileLoaders/pdfFileLoader.js +++ /dev/null @@ -1,60 +0,0 @@ -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; -import FileLoader from './fileLoader'; - -const PDFFileLoader = new (class extends FileLoader { - fileType = 'application/pdf'; - loadFile(file, imageId) { - return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId); - } - - getDataset(image, imageId) { - const dataset = {}; - dataset.imageId = image.imageId || imageId; - return dataset; - } - - getStudies(dataset, imageId) { - return this.getDefaultStudy(imageId); - } - - getDefaultStudy(imageId) { - const study = { - StudyInstanceUID: '', - StudyDate: '', - StudyTime: '', - AccessionNumber: '', - ReferringPhysicianName: '', - PatientName: '', - PatientID: '', - PatientBirthdate: '', - PatientSex: '', - StudyId: '', - StudyDescription: '', - series: [ - { - SeriesInstanceUID: '', - SeriesDescription: '', - SeriesNumber: '', - instances: [ - { - metadata: { - SOPInstanceUID: '', - SOPClassUID: '1.2.840.10008.5.1.4.1.1.104.1', - Rows: '', - Columns: '', - NumberOfFrames: 0, - InstanceNumber: 1, - }, - getImageId: () => imageId, - isLocalFile: true, - }, - ], - }, - ], - }; - - return study; - } -})(); - -export default PDFFileLoader; diff --git a/platform/viewer/src/utils/initWebWorkers.js b/platform/viewer/src/utils/initWebWorkers.js deleted file mode 100644 index 8bbfa28d974..00000000000 --- a/platform/viewer/src/utils/initWebWorkers.js +++ /dev/null @@ -1,22 +0,0 @@ -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; - -let initialized = false; - -export default function initWebWorkers() { - const config = { - maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1), - startWebWorkersOnDemand: true, - taskConfiguration: { - decodeTask: { - initializeCodecsOnStartup: false, - usePDFJS: false, - strict: false, - }, - }, - }; - - if (!initialized) { - cornerstoneWADOImageLoader.webWorkerManager.initialize(config); - initialized = true; - } -} diff --git a/platform/viewer/src/utils/initWebWorkers.test.js b/platform/viewer/src/utils/initWebWorkers.test.js deleted file mode 100644 index cbd554a790d..00000000000 --- a/platform/viewer/src/utils/initWebWorkers.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import initWebWorkers from './initWebWorkers.js'; -import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; - -describe('initWebWorkers', () => { - it("initializes cornerstoneWADOImageLoader's web workers", () => { - initWebWorkers(); - - expect( - cornerstoneWADOImageLoader.webWorkerManager.initialize - ).toHaveBeenCalled(); - }); -}); - -describe('initWebWorkers', () => { - it("initializes cornerstoneWADOImageLoader's web workers only once", () => { - initWebWorkers(); - initWebWorkers(); - - expect( - cornerstoneWADOImageLoader.webWorkerManager.initialize - ).toHaveBeenCalledTimes(1); - }); -});