diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index e7aa1c546..6b33390af 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -4,7 +4,7 @@ import { Template } from 'meteor/templating'; import { ReactiveDict } from 'meteor/reactive-dict'; import { Tracker } from 'meteor/tracker'; import { OHIF } from 'meteor/ohif:core'; -import { MeasurementTable, measurementEvents } from 'meteor/ohif:measurement-table'; +import { MeasurementTable } from 'meteor/ohif:measurement-table'; import 'meteor/ohif:cornerstone'; import 'meteor/ohif:viewerbase'; @@ -168,7 +168,7 @@ Template.viewer.onRendered(function() { Template.viewer.events( Object.assign({ // Viewer Events }, - measurementEvents + MeasurementTable.measurementEvents )); Template.viewer.onDestroyed(function() { diff --git a/OHIFViewer/package-lock.json b/OHIFViewer/package-lock.json index c41cc411b..45492c355 100644 --- a/OHIFViewer/package-lock.json +++ b/OHIFViewer/package-lock.json @@ -34,6 +34,436 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bcrypt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.0.tgz", + "integrity": "sha512-gjicxsD4e5U3nH0EqiEb5y+fKpsZ7F52wcnmNfu45nxnolWVAYh7NgbdfilY+5x1v6cLspxmzz4hf+ju2pFxhA==", + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.2" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "iconv-lite": { + "version": "0.4.23", + "bundled": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.3.3", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "needle": { + "version": "2.2.1", + "bundled": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.2", + "bundled": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.5", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "sax": { + "version": "1.2.4", + "bundled": true + }, + "semver": { + "version": "5.5.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "4.4.4", + "bundled": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.3", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -228,6 +658,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -298,6 +733,11 @@ "util-deprecate": "~1.0.1" } }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", diff --git a/OHIFViewer/package.json b/OHIFViewer/package.json index a357cad00..9063fb260 100644 --- a/OHIFViewer/package.json +++ b/OHIFViewer/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "@babel/runtime": "7.0.0-beta.51", + "bcrypt": "^2.0.0", "loglevel": "^1.6.1" }, "devDependencies": { diff --git a/Packages/ohif-dicomweb-client/src/makeRequest.js b/Packages/ohif-dicomweb-client/src/makeRequest.js index 708c23c5c..dd367ff1d 100644 --- a/Packages/ohif-dicomweb-client/src/makeRequest.js +++ b/Packages/ohif-dicomweb-client/src/makeRequest.js @@ -29,6 +29,14 @@ async function makeRequest(url, options) { }); } + if(options.method) { + requestOpt.method = options.method; + } + + if (options.method === 'POST' && options.body) { + requestOpt.body = options.body; + } + return new Promise((resolve, reject) => { // TODO: Kept getting weird build errors from RegExp const isAbsolute = parsed.href.indexOf('http://') === 0 || parsed.href.indexOf('https://') === 0; diff --git a/Packages/ohif-measurement-table/client/configuration/dataExchange.js b/Packages/ohif-measurement-table/client/configuration/dataExchange.js index 138184047..2a9ce7a9f 100644 --- a/Packages/ohif-measurement-table/client/configuration/dataExchange.js +++ b/Packages/ohif-measurement-table/client/configuration/dataExchange.js @@ -1,5 +1,6 @@ import { OHIF } from 'meteor/ohif:core'; -import { handleSR, getLatestSRSeries } from "../utils/handleSR"; +import { retrieveMeasurementFromSR, stowSRFromMeasurements } from '../utils/handleSR'; +import { getLatestSRSeries } from '../utils/srUtils'; export const retrieveMeasurements = (patientId, timepointIds) => { OHIF.log.info('retrieveMeasurements'); @@ -8,7 +9,7 @@ export const retrieveMeasurements = (patientId, timepointIds) => { if(!latestSeries) return Promise.resolve({}); - return handleSR(latestSeries).then((value) => { + return retrieveMeasurementFromSR(latestSeries).then((value) => { return { length: value } @@ -17,11 +18,17 @@ export const retrieveMeasurements = (patientId, timepointIds) => { export const storeMeasurements = (measurementData, timepointIds) => { OHIF.log.info('storeMeasurements'); + + const server = OHIF.servers.getCurrentServer(); + if (!server || server.type !== 'dicomWeb') { + return Promise.resolve({}); + } - // Here is where we should do any required data transformation and API calls + const studyInstanceUid = measurementData[Object.keys(measurementData)[0]][0].studyInstanceUid - // TODO: Write SR, STOW back to PACS - return Promise.resolve(); + return stowSRFromMeasurements(measurementData).then( () => { + OHIF.studies.deleteStudyMetadataPromise(studyInstanceUid); + }); }; export const retrieveTimepoints = filter => { diff --git a/Packages/ohif-measurement-table/client/index.js b/Packages/ohif-measurement-table/client/index.js index 0847286fc..bf641764e 100644 --- a/Packages/ohif-measurement-table/client/index.js +++ b/Packages/ohif-measurement-table/client/index.js @@ -1,7 +1,6 @@ import { OHIF } from 'meteor/ohif:core'; import { Session } from 'meteor/session'; import { configureApis } from './configuration/configuration' -import { $ } from 'meteor/jquery'; class MeasurementTable { constructor() { @@ -9,8 +8,6 @@ class MeasurementTable { Session.set('TimepointsReady', false); Session.set('MeasurementsReady', false); - - this.firstMeasurementActivated = false; } async onCreated(instance) { @@ -38,6 +35,8 @@ class MeasurementTable { measurementApi.syncMeasurementsAndToolData(); + this.firstMeasurementActivated = false; + instance.autorun(() => { if (!Session.get('TimepointsReady') || !Session.get('MeasurementsReady') || @@ -100,26 +99,25 @@ class MeasurementTable { this.firstMeasurementActivated = true; } -}; - -const measurementEvents = { - 'cornerstonetoolsmeasurementadded .imageViewerViewport'(event, instance) { - const originalEvent = event.originalEvent; - OHIF.measurements.MeasurementHandlers.onAdded(originalEvent, instance); - }, - - 'cornerstonetoolsmeasurementmodified .imageViewerViewport'(event, instance) { - const originalEvent = event.originalEvent; - instance.measurementModifiedHandler(originalEvent, instance); - }, - - 'cornerstonemeasurementremoved .imageViewerViewport'(event, instance) { - const originalEvent = event.originalEvent; - OHIF.measurements.MeasurementHandlers.onRemoved(originalEvent, instance); - } + + static measurementEvents = { + 'cornerstonetoolsmeasurementadded .imageViewerViewport'(event, instance) { + const originalEvent = event.originalEvent; + OHIF.measurements.MeasurementHandlers.onAdded(originalEvent, instance); + }, + + 'cornerstonetoolsmeasurementmodified .imageViewerViewport'(event, instance) { + const originalEvent = event.originalEvent; + instance.measurementModifiedHandler(originalEvent, instance); + }, + + 'cornerstonemeasurementremoved .imageViewerViewport'(event, instance) { + const originalEvent = event.originalEvent; + OHIF.measurements.MeasurementHandlers.onRemoved(originalEvent, instance); + } + }; }; export { - MeasurementTable, - measurementEvents + MeasurementTable } diff --git a/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js index df938922a..a571b577c 100644 --- a/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js +++ b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js @@ -1,26 +1,8 @@ import { OHIF } from 'meteor/ohif:core'; import { cornerstone } from 'meteor/ohif:cornerstone'; +import { getInstanceMetadata } from './srUtils'; -function getInstanceMetadata (displaySets, sopInstanceUid) { - let instance; - - // Use Array.some so that this loop stops when the internal loop - // has found the correct instance - displaySets.some(displaySet => { - // Search the display set to find the instance metadata for - return displaySet.images.find(instanceMetadata => { - if (instanceMetadata._sopInstanceUID === sopInstanceUid) { - instance = instanceMetadata; - - return true; - } - }); - }); - - return instance; -}; - -export default function getLengthMeasurementData(lengthMeasurementContent, displaySets) { +export default getLengthMeasurementData = (lengthMeasurementContent, displaySets) => { let lengthStates = []; lengthMeasurementContent.forEach(groupItemContent => { diff --git a/Packages/ohif-measurement-table/client/utils/getLengthMeasurementGroupContentItem.js b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementGroupContentItem.js new file mode 100644 index 000000000..ff84a5e98 --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementGroupContentItem.js @@ -0,0 +1,79 @@ +export default measurementGroupContentItem = (measurement, sopClassUid) => { + const { + handles, + length, + frameIndex, + sopInstanceUid + } = measurement; + const measurementContentItem = [ + { + RelationshipType: 'HAS OBS CONTEXT', + ValueType: 'TEXT', + ConceptNameCodeSequence: { + CodeValue: '112039', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Tracking Identifier', + }, + TextValue: 'web annotation', + }, + { + RelationshipType: 'HAS OBS CONTEXT', + ValueType: 'UIDREF', + ConceptNameCodeSequence: { + CodeValue: '112040', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Tracking Unique Identifier', + }, + UID: dcmjs.data.DicomMetaDictionary.uid(), + }, + { + RelationshipType: 'CONTAINS', + ValueType: 'CODE', + ConceptNameCodeSequence: { + CodeValue: '121071', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Finding', + }, + ConceptCodeSequence: { + CodeValue: 'SAMPLEFINDING', + CodingSchemeDesignator: '99dcmjs', + CodeMeaning: 'Sample Finding', + }, + }, + { + RelationshipType: 'CONTAINS', + ValueType: 'NUM', + ConceptNameCodeSequence: { + CodeValue: 'G-D7FE', + CodingSchemeDesignator: 'SRT', + CodeMeaning: 'Length', + }, + MeasuredValueSequence: { + MeasurementUnitsCodeSequence: { + CodeValue: 'mm', + CodingSchemeDesignator: 'UCUM', + CodingSchemeVersion: '1.4', + CodeMeaning: 'millimeter', + }, + NumericValue: length, + }, + ContentSequence: { + RelationshipType: 'INFERRED FROM', + ValueType: 'SCOORD', + GraphicType: 'POLYLINE', + GraphicData: [ handles.start.x, handles.start.y, handles.end.x, handles.end.y ], + ContentSequence: { + RelationshipType: 'SELECTED FROM', + ValueType: 'IMAGE', + ReferencedSOPSequence: { + ReferencedSOPClassUID: sopClassUid, + ReferencedSOPInstanceUID: sopInstanceUid, + ReferencedFrameNumber: frameIndex, + } + }, + }, + }, + ]; + + return measurementContentItem; +}; \ No newline at end of file diff --git a/Packages/ohif-measurement-table/client/utils/handleSR.js b/Packages/ohif-measurement-table/client/utils/handleSR.js index 6aef03039..a13165df1 100644 --- a/Packages/ohif-measurement-table/client/utils/handleSR.js +++ b/Packages/ohif-measurement-table/client/utils/handleSR.js @@ -1,100 +1,63 @@ -import { OHIF } from 'meteor/ohif:core'; import { dcmjs } from 'meteor/ohif:cornerstone'; -import getLengthMeasurementData from './getLengthMeasurementData'; +import retrieveDataFromSR from './retrieveDataFromSR'; +import retrieveDataFromMeasurements from './retrieveDataFromMeasurements'; +import { + multipartEncode +} from './srUtils'; -const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22']; - -const toArray = function(x) { - return (x.constructor.name === "Array" ? x : [x]); -}; - -const codeMeaningEquals = (codeMeaningName) => { - return (contentItem) => { - return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName; - }; -}; - -const imagingMeasurementsToMeasurementData = (dataset, displaySets) => { - // Identify the Imaging Measurements - const imagingMeasurementContent = toArray(dataset.ContentSequence).find(codeMeaningEquals("Imaging Measurements")); - - // Retrieve the Measurements themselves - const measurementGroupContent = toArray(imagingMeasurementContent.ContentSequence).find(codeMeaningEquals("Measurement Group")); - - // For now, bail out if the dataset is not a TID1500 SR with length measurements - // TODO: generalize to the various kinds of report - // TODO: generalize to the kinds of measurements the Viewer supports - if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") { - OHIF.log.warn("This package can currently only interpret DICOM SR TID 1500"); - - return {}; - } - - // Filter to find Length measurements in the Structured Report - const lengthMeasurementContent = toArray(measurementGroupContent.ContentSequence).filter(codeMeaningEquals("Length")); - - // Retrieve Length Measurement Data - return getLengthMeasurementData(lengthMeasurementContent, displaySets); -}; - -const retrieveDataFromSR = (Part10SRArrayBuffer) => { - const allStudies = OHIF.viewer.Studies.all(); - let allDisplaySets = []; - - allStudies.forEach(study => { - allDisplaySets = allDisplaySets.concat(study.displaySets); - }); - - // get the dicom data as an Object - let dicomData = dcmjs.data.DicomMessage.readFile(Part10SRArrayBuffer); - let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict); - - // Convert the SR into the kind of object the Measurements package is expecting - return imagingMeasurementsToMeasurementData(dataset, allDisplaySets); -}; - -export const getLatestSRSeries = () => { - const allStudies = OHIF.viewer.StudyMetadataList.all(); - let latestSeries; - - allStudies.forEach(study => { - study.getSeries().forEach(series => { - const firstInstance = series.getFirstInstance(); - const sopClassUid = firstInstance._instance.sopClassUid; - - if (supportedSopClassUIDs.includes(sopClassUid)) { - if(!latestSeries) { - latestSeries = series; - } else if (series._data.seriesDate > latestSeries._data.seriesDate || - (series._data.seriesDate === latestSeries._data.seriesDate && series._data.seriesTime > latestSeries._data.seriesTime)) { - latestSeries = series; - } - } - }); - }); - - return latestSeries; -}; - -export const handleSR = (series) => { +const retrieveMeasurementFromSR = async (series) => { const instance = series.getFirstInstance(); + const options = { + method: 'GET', + responseType: 'arraybuffer', + }; + const url = instance.getDataProperty('wadouri'); - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest(); - - request.responseType = 'arraybuffer'; - request.open('GET', instance.getDataProperty('wadouri')); - - request.onload = function (progressEvent) { - const data = retrieveDataFromSR(progressEvent.currentTarget.response); - - resolve(data); - }; - - request.onerror = function(error) { - reject(error); - }; - - request.send(); - }); + try { + const result = await DICOMWeb.makeRequest(url, options); + const data = await result.arrayBuffer(); + const measurementData = retrieveDataFromSR(data); + return Promise.resolve(measurementData); + } catch(error) { + return Promise.reject(error); + } }; + +/** + * Retrieve the DICOMWeb STOW-RS URL for the current server. + * + * See ftp://medical.nema.org/medical/dicom/current/output/html/part18.html#sect_6.6.1.1 + * + * @return {string|null} The URL, rerouted to the proxy if the WADOProxy is enabled. + */ +function getStowRsUrl() { + const server = OHIF.servers.getCurrentServer(); + const url = `${server.wadoRoot}/studies`; + + return WADOProxy.convertURL(url, server); +} + +const stowSRFromMeasurements = async (measurements) => { + const serverUrl = getStowRsUrl(); + const reportDataset = retrieveDataFromMeasurements(measurements); + const boundary = dcmjs.data.DicomMetaDictionary.uid(); + const options = { + method: 'POST', + body: multipartEncode(reportDataset, boundary), + headers: { + 'Content-Type': `multipart/related; type=application/dicom; boundary=${boundary}` + } + }; + + try { + await DICOMWeb.makeRequest(serverUrl, options); + return Promise.resolve(); + } catch(error) { + return Promise.reject(error); + } +}; + +export { + retrieveMeasurementFromSR, + stowSRFromMeasurements +} diff --git a/Packages/ohif-measurement-table/client/utils/retrieveDataFromMeasurements.js b/Packages/ohif-measurement-table/client/utils/retrieveDataFromMeasurements.js new file mode 100644 index 000000000..94be65f46 --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/retrieveDataFromMeasurements.js @@ -0,0 +1,199 @@ +import { dcmjs } from 'meteor/ohif:cornerstone'; +import { getInstanceMetadata, getAllDisplaySets } from './srUtils'; +import getLengthMeasurementGroupContentItem from './getLengthMeasurementGroupContentItem'; + + +export default retrieveDataFromMeasurements = (measurements) => { + + const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = measurements.allTools[0]; + + const displaySets = getAllDisplaySets(); + const instanceMetadata = getInstanceMetadata(displaySets, sopInstanceUid); + const sopClassUid = instanceMetadata._data.sopClassUid; + + // TODO: figure out what is needed to make a dcmjs dataset from + // information available in the viewer. Apparently the raw dicom is not available + // directly. + const derivationSourceDataset = { + StudyInstanceUID: studyInstanceUid, + SeriesInstanceUID: seriesInstanceUid, + SOPInstanceUID: sopInstanceUid, + SOPClassUID: sopClassUid + }; + report = new dcmjs.derivations.StructuredReport([derivationSourceDataset]); + dataset = report.dataset; + + // TODO: what is the correct metaheader + // http://dicom.nema.org/medical/Dicom/current/output/chtml/part10/chapter_7.html + // TODO: move meta creation to dcmjs + const fileMetaInformationVersionArray = new Uint16Array(1); + fileMetaInformationVersionArray[0] = 1; + dataset._meta = { + FileMetaInformationVersion: fileMetaInformationVersionArray.buffer, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: "1.2.840.10008.1.2.1", // Explicit little endian (always for dcmjs?) + ImplementationClassUID: dcmjs.data.DicomMetaDictionary.uid(), // TODO: could be git hash or other valid id + ImplementationVersionName: "OHIFViewer" + }; + + // TODO: factor out a lot of this back into dcmjs for use in both + // the example and in the Viewer + const measurementGroupContentSequence = []; + for (measurement of measurements.allTools) { + if (measurement.toolType === 'length') { + measurementGroupContentSequence.push.apply(measurementGroupContentSequence, getLengthMeasurementGroupContentItem(measurement, sopClassUid)); + } + } + + // TODO: make a TID1550 derivation as an SR subclass + dataset = Object.assign(dataset, { + ConceptNameCodeSequence: { + CodeValue: '126000', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Imaging Measurement Report' + }, + ContinuityOfContent: 'SEPARATE', + PerformedProcedureCodeSequence: [], + CompletionFlag: 'COMPLETE', + VerificationFlag: 'UNVERIFIED', + ReferencedPerformedProcedureStepSequence: [], + InstanceNumber: 1, + CurrentRequestedProcedureEvidenceSequence: { + StudyInstanceUID: dataset.StudyInstanceUID, + ReferencedSeriesSequence: { + SeriesInstanceUID: dataset.SeriesInstanceUID, + ReferencedSOPSequence: { + ReferencedSOPClassUID: derivationSourceDataset.SOPClassUID, + ReferencedSOPInstanceUID: derivationSourceDataset.SOPInstanceUID + }, + }, + }, + CodingSchemeIdentificationSequence: { + CodingSchemeDesignator: "99dcmjs", + CodingSchemeName: "Codes used for dcmjs", + CodingSchemeVersion: "0", + CodingSchemeResponsibleOrganization: "https://github.com/pieper/dcmjs" + }, + ContentTemplateSequence: { + MappingResource: 'DCMR', + TemplateIdentifier: '1500' + }, + ContentSequence: [ + { + RelationshipType: 'HAS CONCEPT MOD', + ValueType: 'CODE', + ConceptNameCodeSequence: { + CodeValue: '121049', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Language of Content Item and Descendants', + }, + ConceptCodeSequence: { + CodeValue: 'eng', + CodingSchemeDesignator: 'RFC3066', + CodeMeaning: 'English', + }, + ContentSequence: { + RelationshipType: 'HAS CONCEPT MOD', + ValueType: 'CODE', + ConceptNameCodeSequence: { + CodeValue: '121046', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Country of Language', + }, + ConceptCodeSequence: { + CodeValue: 'US', + CodingSchemeDesignator: 'ISO3166_1', + CodeMeaning: 'United States', + } + } + }, + { + RelationshipType: 'HAS OBS CONTEXT', + ValueType: 'PNAME', + ConceptNameCodeSequence: { + CodeValue: '121008', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Person Observer Name', + }, + PersonName: 'user^web' // TODO: these can be options argument for constructor + }, + { + RelationshipType: 'HAS OBS CONTEXT', + ValueType: 'TEXT', + ConceptNameCodeSequence: { + CodeValue: 'RP-100006', + CodingSchemeDesignator: '99dcmjs', + CodeMeaning: "Person Observer's Login Name" + }, + TextValue: 'user' + }, + { + RelationshipType: 'HAS CONCEPT MOD', + ValueType: 'CODE', + ConceptNameCodeSequence: { + CodeValue: '121058', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Procedure reported' + }, + ConceptCodeSequence: { + CodeValue: '1', + CodingSchemeDesignator: '99dcmjs', + CodeMeaning: 'Unknown procedure' + }, + }, + { + RelationshipType: 'CONTAINS', + ValueType: 'CONTAINER', + ConceptNameCodeSequence: { + CodeValue: '111028', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Image Library', + }, + ContinuityOfContent: 'SEPARATE', + ContentSequence: { + RelationshipType: 'CONTAINS', + ValueType: 'CONTAINER', + ConceptNameCodeSequence: { + CodeValue: '126200', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Image Library Group', + }, + ContinuityOfContent: 'SEPARATE', + ContentSequence: { + RelationshipType: 'CONTAINS', + ValueType: 'IMAGE', + ReferencedSOPSequence: { + // TODO: this should refer to the UIDs extracted from the toolState / instance info + ReferencedSOPClassUID: dataset.SOPClassUID, + ReferencedSOPInstanceUID: dataset.SOPInstanceUID, + }, + }, + }, + }, + { + RelationshipType: 'CONTAINS', + ValueType: 'CONTAINER', + ConceptNameCodeSequence: { + CodeValue: '126010', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Imaging Measurements', // TODO: would be nice to abstract the code sequences (in a dictionary? a service?) + }, + ContinuityOfContent: 'SEPARATE', + ContentSequence: { + RelationshipType: 'CONTAINS', + ValueType: 'CONTAINER', + ConceptNameCodeSequence: { + CodeValue: '125007', + CodingSchemeDesignator: 'DCM', + CodeMeaning: 'Measurement Group', + }, + ContinuityOfContent: 'SEPARATE', + ContentSequence: measurementGroupContentSequence, + }, + }, + ] + }); + + return dataset; +} diff --git a/Packages/ohif-measurement-table/client/utils/retrieveDataFromSR.js b/Packages/ohif-measurement-table/client/utils/retrieveDataFromSR.js new file mode 100644 index 000000000..4c643930c --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/retrieveDataFromSR.js @@ -0,0 +1,37 @@ +import { dcmjs } from 'meteor/ohif:cornerstone'; +import getLengthMeasurementData from './getLengthMeasurementData'; +import { toArray, codeMeaningEquals, getAllDisplaySets } from './srUtils' + +const imagingMeasurementsToMeasurementData = (dataset, displaySets) => { + // Identify the Imaging Measurements + const imagingMeasurementContent = toArray(dataset.ContentSequence).find(codeMeaningEquals("Imaging Measurements")); + + // Retrieve the Measurements themselves + const measurementGroupContent = toArray(imagingMeasurementContent.ContentSequence).find(codeMeaningEquals("Measurement Group")); + + // For now, bail out if the dataset is not a TID1500 SR with length measurements + // TODO: generalize to the various kinds of report + // TODO: generalize to the kinds of measurements the Viewer supports + if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") { + OHIF.log.warn("This package can currently only interpret DICOM SR TID 1500"); + + return {}; + } + + // Filter to find Length measurements in the Structured Report + const lengthMeasurementContent = toArray(measurementGroupContent.ContentSequence).filter(codeMeaningEquals("Length")); + + // Retrieve Length Measurement Data + return getLengthMeasurementData(lengthMeasurementContent, displaySets); +}; + +export default retrieveDataFromSR = (Part10SRArrayBuffer) => { + const allDisplaySets = getAllDisplaySets(); + + // get the dicom data as an Object + let dicomData = dcmjs.data.DicomMessage.readFile(Part10SRArrayBuffer); + let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict); + + // Convert the SR into the kind of object the Measurements package is expecting + return imagingMeasurementsToMeasurementData(dataset, allDisplaySets); +}; diff --git a/Packages/ohif-measurement-table/client/utils/srUtils.js b/Packages/ohif-measurement-table/client/utils/srUtils.js new file mode 100644 index 000000000..a82eb2e33 --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/srUtils.js @@ -0,0 +1,105 @@ +import { dcmjs } from 'meteor/ohif:cornerstone'; + +const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22']; + +const toArray = function(x) { + return (x.constructor.name === "Array" ? x : [x]); +}; + +const codeMeaningEquals = (codeMeaningName) => { + return (contentItem) => { + return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName; + }; +}; + +const getAllDisplaySets = () => { + const allStudies = OHIF.viewer.Studies.all(); + let allDisplaySets = []; + + allStudies.forEach(study => { + allDisplaySets = allDisplaySets.concat(study.displaySets); + }); + + return allDisplaySets; +}; + + +const getInstanceMetadata = (displaySets, sopInstanceUid) => { + let instance; + + // Use Array.some so that this loop stops when the internal loop + // has found the correct instance + displaySets.some(displaySet => { + // Search the display set to find the instance metadata for + return displaySet.images.find(instanceMetadata => { + if (instanceMetadata._sopInstanceUID === sopInstanceUid) { + instance = instanceMetadata; + + return true; + } + }); + }); + + return instance; +}; + +const getLatestSRSeries = () => { + const allStudies = OHIF.viewer.StudyMetadataList.all(); + let latestSeries; + + allStudies.forEach(study => { + study.getSeries().forEach(series => { + const firstInstance = series.getFirstInstance(); + const sopClassUid = firstInstance._instance.sopClassUid; + + if (supportedSopClassUIDs.includes(sopClassUid)) { + if(!latestSeries) { + latestSeries = series; + } else if (series._data.seriesDate > latestSeries._data.seriesDate || + (series._data.seriesDate === latestSeries._data.seriesDate && series._data.seriesTime > latestSeries._data.seriesTime)) { + latestSeries = series; + } + } + }); + }); + + return latestSeries; +}; + +const stringToArray = (string) => { + return Uint8Array.from(Array.from(string).map(letter => letter.charCodeAt(0))) +}; + +const multipartEncode = (dataset, boundary) => { + + const denaturalizedMetaheader = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset._meta); + const dicomDict = new dcmjs.data.DicomDict(denaturalizedMetaheader); + + dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset); + + const part10Buffer = dicomDict.write(); + + const header = `\r\n--${boundary}\r\nContent-Type: application/dicom\r\n\r\n`; + const footer = `\r\n--${boundary}--`; + + headerArray = stringToArray(header); + contentArray = new Uint8Array(part10Buffer); + footerArray = stringToArray(footer); + + const multipartArray = new Uint8Array(headerArray.length + contentArray.length + footerArray.length); + + multipartArray.set(headerArray, 0); + multipartArray.set(contentArray, headerArray.length); + multipartArray.set(footerArray, headerArray.length + contentArray.length); + + return(multipartArray.buffer); +}; + +export { + codeMeaningEquals, + getAllDisplaySets, + getInstanceMetadata, + getLatestSRSeries, + multipartEncode, + toArray +} \ No newline at end of file diff --git a/Packages/ohif-measurement-table/package.js b/Packages/ohif-measurement-table/package.js index 3ead375de..6e5c0ccda 100644 --- a/Packages/ohif-measurement-table/package.js +++ b/Packages/ohif-measurement-table/package.js @@ -15,6 +15,7 @@ Package.onUse(function(api) { api.use('ohif:cornerstone-settings'); api.use('ohif:viewerbase'); api.use('ohif:measurements'); + api.use('ohif:wadoproxy'); api.mainModule('client/index.js', 'client'); }); diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html index a72edffed..3147342ef 100644 --- a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html @@ -9,4 +9,21 @@ {{/each}} {{/each}} {{/let}} + {{#if hasAnyMeasurement}} +