From d94f0eec16f203aef2f1dbab162cf865139e0a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20Lelis?= Date: Fri, 20 Jul 2018 07:40:09 -0300 Subject: [PATCH] feat(measurement-table) Display support for measurements from DICOM Structured Reports (#225) --- .../viewerSection/viewerSection.styl | 12 +- OHIFViewer/.meteor/packages | 1 + OHIFViewer/.meteor/versions | 3 + .../flexboxLayout/flexboxLayout.html | 2 +- .../flexboxLayout/flexboxLayout.styl | 14 +- .../toolbarSection/toolbarSection.html | 3 - .../toolbarSection/toolbarSection.js | 21 ++- OHIFViewer/client/components/viewer/viewer.js | 36 ++-- OHIFViewer/package-lock.json | 5 - .../client/lib/classes/MetadataProvider.js | 2 +- Packages/ohif-cornerstone/main.js | 4 +- Packages/ohif-cornerstone/package.js | 4 +- .../ohif-design/styles/imports/variables.styl | 2 +- .../longitudinalViewportOverlay.js | 2 +- .../client/configuration/configuration.js | 27 +++ .../client/configuration/dataExchange.js | 59 +++++++ .../client/configuration/measurementTools.js | 17 ++ .../ohif-measurement-table/client/index.js | 125 ++++++++++++++ .../client/schema/ellipticalRoi.js | 104 ++++++++++++ .../client/schema/length.js | 72 ++++++++ .../client/schema/rectangleRoi.js | 101 ++++++++++++ .../client/schema/toolGroupSchema.js | 29 ++++ .../client/utils/getLengthMeasurementData.js | 95 +++++++++++ .../client/utils/handleSR.js | 100 ++++++++++++ Packages/ohif-measurement-table/package.js | 20 +++ .../both/schema/measurements.js | 6 +- .../both/schema/timepoints.js | 1 + .../client/components/index.js | 1 + .../components/measurementLightTable/index.js | 22 +++ .../measurementEditDescription.html | 13 ++ .../measurementEditDescription.js | 26 +++ .../measurementLightTable.html | 17 ++ .../measurementLightTable.styl | 40 +++++ .../measurementLightTableHeaderRow.html | 6 + .../measurementLightTableHeaderRow.styl | 36 ++++ .../measurementLightTableRow.html | 55 +++++++ .../measurementLightTableRow.js | 103 ++++++++++++ .../measurementLightTableRow.styl | 87 ++++++++++ .../measurementLightTableView.html | 12 ++ .../measurementLightTableView.js | 16 ++ .../measurementLightTableView.styl | 13 ++ .../measurementRelabel.html | 4 + .../measurementRelabel/measurementRelabel.js | 107 ++++++++++++ .../measurementRelabel.styl | 106 ++++++++++++ .../handleSingleMeasurementRemoved.js | 2 +- .../client/lib/getLabelTerminologyList.js | 154 ++++++++++++++++++ .../lib/getMeasurementsGroupedByNumber.js | 10 ++ .../ohif-measurements/client/lib/index.js | 1 + .../client/lib/jumpToRowItem.js | 8 +- .../client/lib/openLocationModal.js | 61 +++++++ Packages/ohif-measurements/package.js | 1 + .../imports/client/lib/loadStudy.js | 17 +- .../client/lib/setActiveViewport.js | 7 +- .../client/lib/viewportUtils.js | 7 +- .../StandaloneViewer/.meteor/packages | 1 + .../StandaloneViewer/.meteor/versions | 4 + .../flexboxLayout/flexboxLayout.html | 5 +- .../components/flexboxLayout/flexboxLayout.js | 3 + .../flexboxLayout/flexboxLayout.styl | 14 +- .../toolbarSection/toolbarSection.html | 3 + .../toolbarSection/toolbarSection.js | 16 ++ .../client/components/viewer/viewer.js | 28 +++- development.Dockerfile | 1 + dockerfile | 1 + 64 files changed, 1797 insertions(+), 78 deletions(-) create mode 100644 Packages/ohif-measurement-table/client/configuration/configuration.js create mode 100644 Packages/ohif-measurement-table/client/configuration/dataExchange.js create mode 100644 Packages/ohif-measurement-table/client/configuration/measurementTools.js create mode 100644 Packages/ohif-measurement-table/client/index.js create mode 100644 Packages/ohif-measurement-table/client/schema/ellipticalRoi.js create mode 100644 Packages/ohif-measurement-table/client/schema/length.js create mode 100644 Packages/ohif-measurement-table/client/schema/rectangleRoi.js create mode 100644 Packages/ohif-measurement-table/client/schema/toolGroupSchema.js create mode 100644 Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js create mode 100644 Packages/ohif-measurement-table/client/utils/handleSR.js create mode 100644 Packages/ohif-measurement-table/package.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/index.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.styl create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.styl create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.styl create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.html create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.js create mode 100644 Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.styl create mode 100644 Packages/ohif-measurements/client/lib/getLabelTerminologyList.js create mode 100644 Packages/ohif-measurements/client/lib/openLocationModal.js diff --git a/LesionTracker/client/components/viewerSection/viewerSection.styl b/LesionTracker/client/components/viewerSection/viewerSection.styl index 21e29f3a3..62802d25d 100644 --- a/LesionTracker/client/components/viewerSection/viewerSection.styl +++ b/LesionTracker/client/components/viewerSection/viewerSection.styl @@ -57,19 +57,19 @@ .sidebar-right flex: 1 - margin-right: - $lesionsSidebarMenuWidth - max-width: $lesionsSidebarMenuWidth + margin-right: - $rightSidebarMenuWidth + max-width: $rightSidebarMenuWidth order: 3 position: relative visibility: hidden &[data-timepoints="3"] - margin-right: - ($lesionsSidebarMenuWidth + 135.5px) - max-width: $lesionsSidebarMenuWidth + 135.5px + margin-right: - ($rightSidebarMenuWidth + 135.5px) + max-width: $rightSidebarMenuWidth + 135.5px &[data-timepoints="4"] - margin-right: - ($lesionsSidebarMenuWidth + 270px) - max-width: $lesionsSidebarMenuWidth + 270px + margin-right: - ($rightSidebarMenuWidth + 270px) + max-width: $rightSidebarMenuWidth + 270px &.sidebar-open margin-right: 0 diff --git a/OHIFViewer/.meteor/packages b/OHIFViewer/.meteor/packages index 5dcf596e4..2339d2a8b 100644 --- a/OHIFViewer/.meteor/packages +++ b/OHIFViewer/.meteor/packages @@ -46,6 +46,7 @@ ohif:hanging-protocols ohif:metadata ohif:user ohif:user-keycloak +ohif:measurement-table fortawesome:fontawesome momentjs:moment@2.15.1 diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions index c5ffbc870..086ba7292 100644 --- a/OHIFViewer/.meteor/versions +++ b/OHIFViewer/.meteor/versions @@ -101,8 +101,11 @@ ohif:hanging-protocols@0.0.1 ohif:header@0.0.1 ohif:hotkeys@0.0.1 ohif:log@0.0.1 +ohif:measurement-table@0.0.1 +ohif:measurements@0.0.1 ohif:metadata@0.0.1 ohif:polyfill@0.0.1 +ohif:select-tree@0.0.1 ohif:servers@0.0.1 ohif:studies@0.0.1 ohif:study-list@0.0.1 diff --git a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html index 28277e263..140da30a1 100644 --- a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html +++ b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html @@ -7,7 +7,7 @@ {{> viewerMain (clone this)}} diff --git a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.styl b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.styl index cedaa7cd4..5e960717a 100644 --- a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.styl +++ b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.styl @@ -1,7 +1,5 @@ @require '{ohif:design}/app' -$lesionsSidebarMenuWidth = 450px - .viewerSection display: flex flex: 1 @@ -47,18 +45,18 @@ $lesionsSidebarMenuWidth = 450px .sidebar-right flex: 1 - margin-right: - $lesionsSidebarMenuWidth - max-width: $lesionsSidebarMenuWidth + margin-right: - $rightSidebarMenuWidth + max-width: $rightSidebarMenuWidth order: 3 position: relative &[data-timepoints="3"] - margin-right: - ($lesionsSidebarMenuWidth + 135.5px) - max-width: $lesionsSidebarMenuWidth + 135.5px + margin-right: - ($rightSidebarMenuWidth + 135.5px) + max-width: $rightSidebarMenuWidth + 135.5px &[data-timepoints="4"] - margin-right: - ($lesionsSidebarMenuWidth + 270px) - max-width: $lesionsSidebarMenuWidth + 270px + margin-right: - ($rightSidebarMenuWidth + 270px) + max-width: $rightSidebarMenuWidth + 270px &.sidebar-open margin-right: 0 diff --git a/OHIFViewer/client/components/toolbarSection/toolbarSection.html b/OHIFViewer/client/components/toolbarSection/toolbarSection.html index b652328eb..067a9f914 100644 --- a/OHIFViewer/client/components/toolbarSection/toolbarSection.html +++ b/OHIFViewer/client/components/toolbarSection/toolbarSection.html @@ -8,9 +8,6 @@
{{>roundedButtonGroup rightSidebarToggleButtonData}}
-
- {{>toolbarSectionTools toolbarButtons=hangingProtocolButtons}} -
diff --git a/OHIFViewer/client/components/toolbarSection/toolbarSection.js b/OHIFViewer/client/components/toolbarSection/toolbarSection.js index 6641c02d4..9d0eb3601 100644 --- a/OHIFViewer/client/components/toolbarSection/toolbarSection.js +++ b/OHIFViewer/client/components/toolbarSection/toolbarSection.js @@ -47,14 +47,29 @@ Template.toolbarSection.helpers({ rightSidebarToggleButtonData() { const instance = Template.instance(); + + // TODO: Figured out a way to handle both right panel contents + // return { + // toggleable: true, + // key: 'rightSidebar', + // value: instance.data.state, + // options: [{ + // value: 'hangingprotocols', + // iconClasses: 'fa fa-cog', + // bottomLabel: 'Hanging' + // }] + // }; + return { toggleable: true, key: 'rightSidebar', value: instance.data.state, options: [{ - value: 'hangingprotocols', - iconClasses: 'fa fa-cog', - bottomLabel: 'Hanging' + value: 'measurements', + svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-measurements-lesions', + svgWidth: 18, + svgHeight: 10, + bottomLabel: 'Measurements' }] }; }, diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index 67edd5621..e7aa1c546 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -4,6 +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 'meteor/ohif:cornerstone'; import 'meteor/ohif:viewerbase'; @@ -64,8 +65,6 @@ Meteor.startup(() => { invert: viewportUtils.invert }; - OHIF.viewer.stackImagePositionOffsetSynchronizer = new OHIF.viewerbase.StackImagePositionOffsetSynchronizer(); - // Create the synchronizer used to update reference lines OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('cornerstonenewimage', cornerstoneTools.updateImageSynchronizer); @@ -74,6 +73,9 @@ Meteor.startup(() => { // Metadata configuration const metadataProvider = OHIF.viewer.metadataProvider; cornerstone.metaData.addProvider(metadataProvider.provider.bind(metadataProvider)); + + // Instanciate viewer plugins + OHIF.viewer.measurementTable = new MeasurementTable(); }); Template.viewer.onCreated(() => { @@ -135,10 +137,15 @@ Template.viewer.onCreated(() => { // Updates WADO-RS metaDataManager OHIF.viewerbase.updateMetaDataManager(study); }); + + // Call Viewer plugins onCreated functions + if(typeof OHIF.viewer.measurementTable.onCreated === 'function') { + OHIF.viewer.measurementTable.onCreated(instance); + } }); Template.viewer.onRendered(function() { - + const instance = Template.instance(); this.autorun(function() { // To make sure ohif viewerMain is rendered before initializing Hanging Protocols const isOHIFViewerMainRendered = Session.get('OHIFViewerMainRendered'); @@ -151,20 +158,23 @@ Template.viewer.onRendered(function() { } }); + // Call Viewer plugins onRendered functions + if(typeof OHIF.viewer.measurementTable.onRendered === 'function') { + OHIF.viewer.measurementTable.onRendered(instance); + } + }); -Template.viewer.events({ - 'click .js-toggle-studies'() { - const instance = Template.instance(); - const current = instance.state.get('leftSidebar'); - instance.state.set('leftSidebar', !current); +Template.viewer.events( Object.assign({ + // Viewer Events }, + measurementEvents +)); - 'click .js-toggle-protocol-editor'() { - const instance = Template.instance(); - const current = instance.state.get('rightSidebar'); - instance.data.state.set('rightSidebar', !current); - }, +Template.viewer.onDestroyed(function() { + if(typeof OHIF.viewer.measurementTable.onDestroyed === 'function') { + OHIF.viewer.measurementTable.onDestroyed(); + } }); Template.viewer.helpers({ diff --git a/OHIFViewer/package-lock.json b/OHIFViewer/package-lock.json index b7ed35c44..c41cc411b 100644 --- a/OHIFViewer/package-lock.json +++ b/OHIFViewer/package-lock.json @@ -298,11 +298,6 @@ "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/Packages/ohif-cornerstone-settings/imports/client/lib/classes/MetadataProvider.js b/Packages/ohif-cornerstone-settings/imports/client/lib/classes/MetadataProvider.js index 6642b928d..159f6ce69 100644 --- a/Packages/ohif-cornerstone-settings/imports/client/lib/classes/MetadataProvider.js +++ b/Packages/ohif-cornerstone-settings/imports/client/lib/classes/MetadataProvider.js @@ -118,7 +118,7 @@ export class MetadataProvider { value = image.instance[attrName]; } - return value == null ? defaultValue : value; + return value === null ? defaultValue : value; } getFromDataSet(dataSet, type, tag) { diff --git a/Packages/ohif-cornerstone/main.js b/Packages/ohif-cornerstone/main.js index a50ea3a84..b87effb58 100644 --- a/Packages/ohif-cornerstone/main.js +++ b/Packages/ohif-cornerstone/main.js @@ -4,6 +4,7 @@ import * as cornerstoneMath from 'cornerstone-math/dist/cornerstoneMath.js'; import * as cornerstoneTools from 'cornerstone-tools/dist/cornerstoneTools.js'; import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader/dist/cornerstoneWADOImageLoader.js'; import * as dicomParser from 'dicom-parser'; // Importing from dist breaks instance reference of dicomParser.DataSet class +import * as dcmjs from 'dcmjs/build/dcmjs'; cornerstoneTools.external.Hammer = Hammer; cornerstoneTools.external.cornerstone = cornerstone; @@ -18,5 +19,6 @@ export { cornerstoneTools, cornerstoneMath, cornerstoneWADOImageLoader, - dicomParser + dicomParser, + dcmjs }; diff --git a/Packages/ohif-cornerstone/package.js b/Packages/ohif-cornerstone/package.js index 7efd862cc..5cfc7a721 100644 --- a/Packages/ohif-cornerstone/package.js +++ b/Packages/ohif-cornerstone/package.js @@ -10,7 +10,8 @@ Npm.depends({ 'cornerstone-tools': '2.3.3', 'cornerstone-math': '0.1.6', 'dicom-parser': '1.8.0', - 'cornerstone-wado-image-loader': '2.1.4' + 'cornerstone-wado-image-loader': '2.1.4', + 'dcmjs': '0.1.3' }); Package.onUse(function(api) { @@ -29,4 +30,5 @@ Package.onUse(function(api) { api.export('cornerstoneTools', 'client'); api.export('cornerstoneWADOImageLoader', 'client'); api.export('dicomParser', 'client'); + api.export('dcmjs', 'client'); }); diff --git a/Packages/ohif-design/styles/imports/variables.styl b/Packages/ohif-design/styles/imports/variables.styl index e38c7f22b..4e0bace73 100644 --- a/Packages/ohif-design/styles/imports/variables.styl +++ b/Packages/ohif-design/styles/imports/variables.styl @@ -6,7 +6,7 @@ $topBarExpandedHeight = 160px $toolbarHeight = 78px $toolbarDrawerHeight = 62px $studiesSidebarMenuWidth = 307px -$lesionsSidebarMenuWidth = 323px +$rightSidebarMenuWidth = 323px $studyListPadding = 8% $studyListPaddingMediumScreen = 10px diff --git a/Packages/ohif-lesiontracker/client/components/longitudinal/longitudinalViewportOverlay/longitudinalViewportOverlay.js b/Packages/ohif-lesiontracker/client/components/longitudinal/longitudinalViewportOverlay/longitudinalViewportOverlay.js index 8409c08a8..e3a0dab44 100644 --- a/Packages/ohif-lesiontracker/client/components/longitudinal/longitudinalViewportOverlay/longitudinalViewportOverlay.js +++ b/Packages/ohif-lesiontracker/client/components/longitudinal/longitudinalViewportOverlay/longitudinalViewportOverlay.js @@ -30,7 +30,7 @@ Template[defaultTemplate].onRendered(() => { const instance = Template.instance(); const { studyInstanceUid, seriesInstanceUid } = instance.data; - Tracker.autorun(computation => { + instance.autorun(computation => { Session.get('CornerstoneNewImage' + instance.data.viewportIndex); if (computation.firstRun) return; computation.stop(); diff --git a/Packages/ohif-measurement-table/client/configuration/configuration.js b/Packages/ohif-measurement-table/client/configuration/configuration.js new file mode 100644 index 000000000..20c6ac54b --- /dev/null +++ b/Packages/ohif-measurement-table/client/configuration/configuration.js @@ -0,0 +1,27 @@ +import { OHIF } from 'meteor/ohif:core'; + +import { measurementTools } from './measurementTools'; +import { retrieveMeasurements, storeMeasurements, retrieveTimepoints, storeTimepoints, removeTimepoint, updateTimepoint, disassociateStudy } from './dataExchange'; + +export const configureApis = () => { + OHIF.measurements.MeasurementApi.setConfiguration({ + measurementTools, + dataExchange: { + retrieve: retrieveMeasurements, + store: storeMeasurements + }, + dataValidation: { + validation: () => {} + } + }); + + OHIF.measurements.TimepointApi.setConfiguration({ + dataExchange: { + retrieve: retrieveTimepoints, + store: storeTimepoints, + remove: removeTimepoint, + update: updateTimepoint, + disassociate: disassociateStudy + } + }); +}; diff --git a/Packages/ohif-measurement-table/client/configuration/dataExchange.js b/Packages/ohif-measurement-table/client/configuration/dataExchange.js new file mode 100644 index 000000000..138184047 --- /dev/null +++ b/Packages/ohif-measurement-table/client/configuration/dataExchange.js @@ -0,0 +1,59 @@ +import { OHIF } from 'meteor/ohif:core'; +import { handleSR, getLatestSRSeries } from "../utils/handleSR"; + +export const retrieveMeasurements = (patientId, timepointIds) => { + OHIF.log.info('retrieveMeasurements'); + + const latestSeries = getLatestSRSeries(); + + if(!latestSeries) return Promise.resolve({}); + + return handleSR(latestSeries).then((value) => { + return { + length: value + } + }); +}; + +export const storeMeasurements = (measurementData, timepointIds) => { + OHIF.log.info('storeMeasurements'); + + // Here is where we should do any required data transformation and API calls + + // TODO: Write SR, STOW back to PACS + return Promise.resolve(); +}; + +export const retrieveTimepoints = filter => { + const studyInstanceUids = OHIF.viewer.StudyMetadataList.all().map(study => study.getStudyInstanceUID()); + OHIF.log.info('retrieveTimepoints'); + + return Promise.resolve([{ + timepointType: 'baseline', + timepointId: 'TimepointId', + studyInstanceUids, + patientId: filter.patientId, + earliestDate: new Date(), + latestDate: new Date(), + isLocked: false + }]); +}; + +export const storeTimepoints = (timepointData) => { + OHIF.log.info('storeTimepoints'); + return Promise.resolve(); +}; + +export const updateTimepoint = (timepointData, query) => { + OHIF.log.info('updateTimepoint'); + return Promise.resolve(); +}; + +export const removeTimepoint = timepointId => { + OHIF.log.info('removeTimepoint'); + return Promise.resolve(); +}; + +export const disassociateStudy = (timepointIds, studyInstanceUid) => { + return Promise.resolve(); +}; diff --git a/Packages/ohif-measurement-table/client/configuration/measurementTools.js b/Packages/ohif-measurement-table/client/configuration/measurementTools.js new file mode 100644 index 000000000..4d4e2651a --- /dev/null +++ b/Packages/ohif-measurement-table/client/configuration/measurementTools.js @@ -0,0 +1,17 @@ +import { ToolGroupBaseSchema } from '../schema/toolGroupSchema'; +import { length } from '../schema/length'; +import { ellipticalRoi } from '../schema/ellipticalRoi'; +import { rectangleRoi } from '../schema/rectangleRoi'; + +const trackedTools = [ + length, + ellipticalRoi, + rectangleRoi +]; + +export const measurementTools = [{ + id: 'allTools', + name: 'Measurements', + childTools: trackedTools, + schema: ToolGroupBaseSchema +}]; diff --git a/Packages/ohif-measurement-table/client/index.js b/Packages/ohif-measurement-table/client/index.js new file mode 100644 index 000000000..0847286fc --- /dev/null +++ b/Packages/ohif-measurement-table/client/index.js @@ -0,0 +1,125 @@ +import { OHIF } from 'meteor/ohif:core'; +import { Session } from 'meteor/session'; +import { configureApis } from './configuration/configuration' +import { $ } from 'meteor/jquery'; + +class MeasurementTable { + constructor() { + configureApis(); + + Session.set('TimepointsReady', false); + Session.set('MeasurementsReady', false); + + this.firstMeasurementActivated = false; + } + + async onCreated(instance) { + const { TimepointApi, MeasurementApi } = OHIF.measurements; + + OHIF.viewer.data.currentTimepointId = 'TimepointId'; + + const timepointApi = new TimepointApi(OHIF.viewer.data.currentTimepointId); + const measurementApi = new MeasurementApi(timepointApi); + const apis = { + timepointApi, + measurementApi + }; + + Object.assign(OHIF.viewer, apis); + Object.assign(instance.data, apis); + + const patientId = instance.data.studies[0].patientId; + + await timepointApi.retrieveTimepoints({ patientId }); + Session.set('TimepointsReady', true); + + await measurementApi.retrieveMeasurements(patientId, [OHIF.viewer.data.currentTimepointId]); + Session.set('MeasurementsReady', true); + + measurementApi.syncMeasurementsAndToolData(); + + instance.autorun(() => { + if (!Session.get('TimepointsReady') || + !Session.get('MeasurementsReady') || + !Session.get('ViewerReady') || + this.firstMeasurementActivated) { + return; + } + + this.jumpToFirstMeasurement(); + + }); + + instance.measurementModifiedHandler = _.throttle((event, instance) => { + OHIF.measurements.MeasurementHandlers.onModified(event, instance); + }, 300); + } + + onDestroyed() { + Session.set('TimepointsReady', false); + Session.set('MeasurementsReady', false); + } + + jumpToFirstMeasurement() { + // Find and activate the first measurement by Lesion Number + // NOTE: This is inefficient, we should be using a hanging protocol + // to hang the first measurement's imageId immediately, rather + // than changing images after initial loading... + const config = OHIF.measurements.MeasurementApi.getConfiguration(); + const tools = config.measurementTools[0].childTools; + const firstTool = tools[Object.keys(tools)[0]]; + const measurementTypeId = firstTool.id; + + const collection = OHIF.viewer.measurementApi.tools[measurementTypeId]; + const sorting = { + sort: { + measurementNumber: -1 + } + }; + + const data = collection.find({}, sorting).fetch(); + + // TODO: Clean this up, it's probably an inefficient way to get what we need + const groupObject = _.groupBy(data, m => m.measurementNumber); + + // Reformat the data + const rows = Object.keys(groupObject).map(key => ({ + measurementTypeId: measurementTypeId, + measurementNumber: key, + entries: groupObject[key] + })); + + const rowItem = rows[0]; + const timepoints = [ + OHIF.viewer.timepointApi.current() + ]; + + if (rowItem) { + OHIF.measurements.jumpToRowItem(rowItem, timepoints); + } + + 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); + } +}; + +export { + MeasurementTable, + measurementEvents +} diff --git a/Packages/ohif-measurement-table/client/schema/ellipticalRoi.js b/Packages/ohif-measurement-table/client/schema/ellipticalRoi.js new file mode 100644 index 000000000..5baefc818 --- /dev/null +++ b/Packages/ohif-measurement-table/client/schema/ellipticalRoi.js @@ -0,0 +1,104 @@ +import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements'; + +const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema; + +const EllipticalRoiHandlesSchema = new SimpleSchema({ + start: { + type: CornerstoneHandleSchema, + label: 'Start' + }, + end: { + type: CornerstoneHandleSchema, + label: 'End' + }, + textBox: { + type: CornerstoneHandleSchema, + label: 'Text Box' + }, +}); + +const MeanStdDevSchema = new SimpleSchema({ + count: { + type: Number, + label: 'count', + decimal: true + }, + mean: { + type: Number, + label: 'mean', + decimal: true + }, + stdDev: { + type: Number, + label: 'stdDev', + decimal: true + }, + variance: { + type: Number, + label: 'variance', + decimal: true + } + +}); + +const EllipticalRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, { + handles: { + type: EllipticalRoiHandlesSchema, + label: 'Handles' + }, + measurementNumber: { + type: Number, + label: 'Measurement Number' + }, + toolType: { + type: String, + label: 'Measurement Tool Type', + defaultValue: 'ellipticalRoi' + }, + location: { + type: String, + label: 'Location', + optional: true + }, + description: { + type: String, + label: 'Description', + optional: true + }, + area: { + type: Number, + label: 'Ellipse Area value', + decimal: true, + optional: true + }, + meanStdDev: { + type: MeanStdDevSchema, + label: 'MeanStd Values', + optional: true + } +}]); + +const displayFunction = data => { + let meanValue = ''; + if (data.meanStdDev && data.meanStdDev.mean) { + meanValue = data.meanStdDev.mean.toFixed(2) + ' HU'; + } + return meanValue; + // let meanValue = data.meanStdDev && data.meanStdDev.mean || 0; + // return numberWithCommas(meanValue).toFixed(2) + ' HU'; + //return data.meanStdDev.mean.toFixed(2); +}; + +export const ellipticalRoi = { + id: 'ellipticalRoi', + name: 'Ellipse', + toolGroup: 'allTools', + cornerstoneToolType: 'ellipticalRoi', + schema: EllipticalRoiSchema, + options: { + measurementTable: { + displayFunction + } + } +}; diff --git a/Packages/ohif-measurement-table/client/schema/length.js b/Packages/ohif-measurement-table/client/schema/length.js new file mode 100644 index 000000000..f56a224c4 --- /dev/null +++ b/Packages/ohif-measurement-table/client/schema/length.js @@ -0,0 +1,72 @@ +import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements'; + +const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema; + +const LengthHandlesSchema = new SimpleSchema({ + start: { + type: CornerstoneHandleSchema, + label: 'Start' + }, + end: { + type: CornerstoneHandleSchema, + label: 'End' + }, + textBox: { + type: CornerstoneHandleSchema, + label: 'Text Box' + } +}); + +const LengthSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, { + handles: { + type: LengthHandlesSchema, + label: 'Handles' + }, + measurementNumber: { + type: Number, + label: 'Measurement Number' + }, + location: { + type: String, + label: 'Location', + optional: true + }, + description: { + type: String, + label: 'Description', + optional: true + }, + toolType: { + type: String, + label: 'Measurement Tool Type', + defaultValue: 'length' + }, + length: { + type: Number, + label: 'Length', + optional: true, + decimal: true + } +}]); + +const displayFunction = data => { + let lengthValue = ''; + if (data.length) { + lengthValue = data.length.toFixed(2) + ' mm'; + } + return lengthValue; +}; + +export const length = { + id: 'length', + name: 'Length', + toolGroup: 'allTools', + cornerstoneToolType: 'length', + schema: LengthSchema, + options: { + measurementTable: { + displayFunction + } + } +}; diff --git a/Packages/ohif-measurement-table/client/schema/rectangleRoi.js b/Packages/ohif-measurement-table/client/schema/rectangleRoi.js new file mode 100644 index 000000000..e161e75d1 --- /dev/null +++ b/Packages/ohif-measurement-table/client/schema/rectangleRoi.js @@ -0,0 +1,101 @@ +import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements'; + +const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema; + +const RectangleRoiHandlesSchema = new SimpleSchema({ + start: { + type: CornerstoneHandleSchema, + label: 'Start' + }, + end: { + type: CornerstoneHandleSchema, + label: 'End' + }, + textBox: { + type: CornerstoneHandleSchema, + label: 'Text Box' + }, +}); + +const MeanStdDevSchema = new SimpleSchema({ + count: { + type: Number, + label: 'count', + decimal: true + }, + mean: { + type: Number, + label: 'mean', + decimal: true + }, + stdDev: { + type: Number, + label: 'stdDev', + decimal: true + }, + variance: { + type: Number, + label: 'variance', + decimal: true + } + +}); + +const RectangleRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, { + handles: { + type: RectangleRoiHandlesSchema, + label: 'Handles' + }, + measurementNumber: { + type: Number, + label: 'Measurement Number' + }, + toolType: { + type: String, + label: 'Measurement Tool Type', + defaultValue: 'rectangleRoi' + }, + location: { + type: String, + label: 'Location', + optional: true + }, + description: { + type: String, + label: 'Description', + optional: true + }, + area: { + type: Number, + label: 'Rectangle Area value', + decimal: true, + optional: true + }, + meanStdDev: { + type: MeanStdDevSchema, + label: 'MeanStd Values', + optional: true + } +}]); + +const displayFunction = data => { + let meanValue = ''; + if (data.meanStdDev && data.meanStdDev.mean) { + meanValue = data.meanStdDev.mean.toFixed(2) + ' HU'; + } + return meanValue; +}; + +export const rectangleRoi = { + id: 'rectangleRoi', + name: 'Rectangle', + toolGroup: 'allTools', + cornerstoneToolType: 'rectangleRoi', + schema: RectangleRoiSchema, + options: { + measurementTable: { + displayFunction + } + } +}; diff --git a/Packages/ohif-measurement-table/client/schema/toolGroupSchema.js b/Packages/ohif-measurement-table/client/schema/toolGroupSchema.js new file mode 100644 index 000000000..22576a52c --- /dev/null +++ b/Packages/ohif-measurement-table/client/schema/toolGroupSchema.js @@ -0,0 +1,29 @@ +import { SimpleSchema } from 'meteor/aldeed:simple-schema'; + +export const ToolGroupBaseSchema = new SimpleSchema({ + toolId: { + type: String, + label: 'Tool ID', + optional: true + }, + toolItemId: { + type: String, + label: 'Tool Item ID', + optional: true + }, + createdAt: { + type: Date + }, + studyInstanceUid: { + type: String, + label: 'Study Instance UID' + }, + timepointId: { + type: String, + label: 'Timepoint ID' + }, + measurementNumber: { + type: Number, + label: 'Measurement Number' + } +}); diff --git a/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js new file mode 100644 index 000000000..df938922a --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/getLengthMeasurementData.js @@ -0,0 +1,95 @@ +import { OHIF } from 'meteor/ohif:core'; +import { cornerstone } from 'meteor/ohif:cornerstone'; + +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) { + let lengthStates = []; + + lengthMeasurementContent.forEach(groupItemContent => { + const lengthContent = groupItemContent.ContentSequence; + const reference = lengthContent.ContentSequence.ReferencedSOPSequence; + const lengthState = {}; + + lengthState.measuredValue = groupItemContent.MeasuredValueSequence.NumericValue; + lengthState.handles = {start: {}, end: {}}; + [lengthState.handles.start.x, + lengthState.handles.start.y, + lengthState.handles.end.x, + lengthState.handles.end.y] = lengthContent.GraphicData; + + // TODO: Save textbox position in GraphicData? + lengthState.handles.textBox = { + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + } + + lengthState.ReferencedInstanceUID = reference.ReferencedSOPInstanceUID; + if (reference.ReferencedFrameNumber && reference.ReferencedFrameNumber !== 'NaN') { + lengthState.ReferencedFrameNumber = reference.ReferencedFrameNumber; + } else { + lengthState.ReferencedFrameNumber = 0; + } + + lengthStates.push(lengthState); + }); + + const lengthMeasurementData = []; + + let measurementNumber = 0; + lengthStates.forEach(lengthState => { + const sopInstanceUid = lengthState.ReferencedInstanceUID; + const instanceMetadata = getInstanceMetadata(displaySets, sopInstanceUid); + const imageId = OHIF.viewerbase.getImageId(instanceMetadata); + if (!imageId) { + return; + } + + const studyInstanceUid = cornerstone.metaData.get('study', imageId).studyInstanceUid; + const seriesInstanceUid = cornerstone.metaData.get('series', imageId).seriesInstanceUid; + const patientId = instanceMetadata._study.patientId; + const frameIndex = lengthState.ReferencedFrameNumber; + const imagePath = [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_'); + const measurement = { + handles: lengthState.handles, + length: lengthState.measuredValue, + imageId, + imagePath, + sopInstanceUid, + seriesInstanceUid, + studyInstanceUid, + patientId, + frameIndex, + measurementNumber: ++measurementNumber, + userId: 'UserID', + timepointId: OHIF.viewer.data.currentTimepointId, + toolType: 'length', + _id: imageId + measurementNumber, + }; + + lengthMeasurementData.push(measurement); + }); + + + return lengthMeasurementData; +}; diff --git a/Packages/ohif-measurement-table/client/utils/handleSR.js b/Packages/ohif-measurement-table/client/utils/handleSR.js new file mode 100644 index 000000000..6aef03039 --- /dev/null +++ b/Packages/ohif-measurement-table/client/utils/handleSR.js @@ -0,0 +1,100 @@ +import { OHIF } from 'meteor/ohif:core'; +import { dcmjs } from 'meteor/ohif:cornerstone'; +import getLengthMeasurementData from './getLengthMeasurementData'; + +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 instance = series.getFirstInstance(); + + 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(); + }); +}; diff --git a/Packages/ohif-measurement-table/package.js b/Packages/ohif-measurement-table/package.js new file mode 100644 index 000000000..3ead375de --- /dev/null +++ b/Packages/ohif-measurement-table/package.js @@ -0,0 +1,20 @@ +Package.describe({ + name: 'ohif:measurement-table', + summary: 'OHIF Measurement table', + version: '0.0.1' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.6'); + + api.use('ecmascript'); + + // Our custom packages + api.use('ohif:cornerstone'); + api.use('ohif:core'); + api.use('ohif:cornerstone-settings'); + api.use('ohif:viewerbase'); + api.use('ohif:measurements'); + + api.mainModule('client/index.js', 'client'); +}); diff --git a/Packages/ohif-measurements/both/schema/measurements.js b/Packages/ohif-measurements/both/schema/measurements.js index c7d3982a4..001b539d1 100644 --- a/Packages/ohif-measurements/both/schema/measurements.js +++ b/Packages/ohif-measurements/both/schema/measurements.js @@ -10,11 +10,13 @@ const Measurement = new SimpleSchema({ }, userId: { type: String, - label: 'User ID' + label: 'User ID', + optional: true }, patientId: { type: String, - label: 'Patient ID' + label: 'Patient ID', + optional: true }, measurementNumber: { type: Number, diff --git a/Packages/ohif-measurements/both/schema/timepoints.js b/Packages/ohif-measurements/both/schema/timepoints.js index 78a13d888..c1828b64c 100644 --- a/Packages/ohif-measurements/both/schema/timepoints.js +++ b/Packages/ohif-measurements/both/schema/timepoints.js @@ -4,6 +4,7 @@ export const schema = new SimpleSchema({ patientId: { type: String, label: 'Patient ID', + optional: true }, timepointId: { type: String, diff --git a/Packages/ohif-measurements/client/components/index.js b/Packages/ohif-measurements/client/components/index.js index 8ae03cb94..098dc567d 100644 --- a/Packages/ohif-measurements/client/components/index.js +++ b/Packages/ohif-measurements/client/components/index.js @@ -1,4 +1,5 @@ import './association'; import './caseProgress'; import './measurementTable'; +import './measurementLightTable'; import './measureFlow'; diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/index.js b/Packages/ohif-measurements/client/components/measurementLightTable/index.js new file mode 100644 index 000000000..cc373668e --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/index.js @@ -0,0 +1,22 @@ +// Measurement Table Components imports + +import './measurementEditDescription/measurementEditDescription.html'; +import './measurementEditDescription/measurementEditDescription.js'; + +import './measurementLightTable.html'; +import './measurementLightTable.styl'; + +import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.html'; +import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl'; + +import './measurementLightTableRow/measurementLightTableRow.html'; +import './measurementLightTableRow/measurementLightTableRow.styl'; +import './measurementLightTableRow/measurementLightTableRow.js'; + +import './measurementLightTableView/measurementLightTableView.html'; +import './measurementLightTableView/measurementLightTableView.styl'; +import './measurementLightTableView/measurementLightTableView.js'; + +import './measurementRelabel/measurementRelabel.html'; +import './measurementRelabel/measurementRelabel.styl'; +import './measurementRelabel/measurementRelabel.js'; diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.html new file mode 100644 index 000000000..e9f128fad --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.html @@ -0,0 +1,13 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.js b/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.js new file mode 100644 index 000000000..5b7fb3183 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementEditDescription/measurementEditDescription.js @@ -0,0 +1,26 @@ +import { Template } from 'meteor/templating'; +import { OHIF } from 'meteor/ohif:core'; + + +Template.measurementEditDescription.onRendered(() => { + const instance = Template.instance(); + const form = instance.$('form').data('component'); + const measurementData = instance.data.measurementData; + const collection = OHIF.viewer.measurementApi.tools[measurementData.toolType]; + const currentMeasurement = collection.findOne({ _id: measurementData._id }); + + if (currentMeasurement.description) { + form.value({ + description: currentMeasurement.description + }); + } + + // Update the description after confirming the dialog data + instance.data.promise.then(formData => { + collection.update({ + _id: measurementData._id, + }, { + $set: { description: formData.description } + }); + }); +}); diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.html new file mode 100644 index 000000000..cb8af2a3f --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.html @@ -0,0 +1,17 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.styl b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.styl new file mode 100644 index 000000000..d8e64e59b --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTable.styl @@ -0,0 +1,40 @@ +@require '{ohif:design}/app' + +#measurementLightTableContainer + theme('background-color', '$primaryBackgroundColor') + height: 100% + width: 100% + +.measurementLightTableHeaderContainer + display: flex + padding: 0 2px 0 44px + position: relative + + .measurementLightTableHeader + flex: 1 + justify-content: space-around + padding-top: 2px + position: relative + margin: 0.4em 0 + + .studyDateLabel, .studyDate + theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness) + font-weight: 400 + padding-left: 12px + text-align: left + + .studyDateLabel + theme('color', '$textSecondaryColor') + font-size: 12px + line-height: 12px + + .studyDate + theme('color', '$textPrimaryColor') + font-size: 14px + line-height: 20px + padding-bottom: 6px + +.measurementLightTableLayoutChanger + margin: 0 auto + padding: 20px + text-align: center diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.html new file mode 100644 index 000000000..3a56bdeb7 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.html @@ -0,0 +1,6 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl new file mode 100644 index 000000000..d11f9d255 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl @@ -0,0 +1,36 @@ +@import "{ohif:design}/app" + +$headerRowHeight = 63px + +.measurementLightTableHeaderRow + theme('background-color', '$uiGrayDarker') + theme('color', '$textSecondaryColor') + display: flex + theme('fill', '$textSecondaryColor') + height: $headerRowHeight + line-height: $headerRowHeight + margin-top: 2px + width: 100% + + div + align-items: stretch + flex: 1 + justify-content: space-around + text-align: center + + .type + theme('color', '$textSecondaryColor') + font-size: 22px + font-weight: 300 + line-height: $headerRowHeight + padding: 0 10px + text-align: left + + .numberOfMeasurements + theme('color', '$uiSkyBlue') + float: right + font-weight: 300 + font-size: 40px + max-width: 54px + height: $headerRowHeight + line-height: 66px diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.html new file mode 100644 index 000000000..0cb324b0f --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.html @@ -0,0 +1,55 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.js b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.js new file mode 100644 index 000000000..83f687fd5 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.js @@ -0,0 +1,103 @@ +import { Template } from 'meteor/templating'; +import { _ } from 'meteor/underscore'; +import { $ } from 'meteor/jquery'; +import { OHIF } from 'meteor/ohif:core'; +import { cornerstone } from 'meteor/ohif:cornerstone'; + +const getPosition = event => { + return { + x: event.clientX, + y: event.clientY + }; +}; + +Template.measurementLightTableRow.helpers({ + displayData() { + const instance = Template.instance(); + const { rowItem } = instance.data; + + const data = rowItem.entries[0]; + + const config = OHIF.measurements.MeasurementApi.getConfiguration(); + const measurementTools = config.measurementTools; + + const toolGroup = measurementTools.find( toolGroup => toolGroup.id === rowItem.measurementTypeId); + const tool = toolGroup.childTools.find( childTool => childTool.id === data.toolType ); + if (!tool) { + return 'No measurement value'; + } + + const { displayFunction } = tool.options.measurementTable; + return displayFunction(data); + } +}); + +Template.measurementLightTableRow.events({ + 'click .measurementLightTableRow'(event, instance) { + const $row = instance.$('.measurementLightTableRow'); + const rowItem = instance.data.rowItem; + const timepoints = instance.data.timepointApi.all(); + + $row.closest('.measurementLightTableView').find('.measurementLightTableRow').not($row).removeClass('active'); + $row.toggleClass('active'); + + const childToolKey = $(event.target).attr('data-child'); + OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey); + }, + + 'click .js-edit-label'(event, instance) { + event.stopPropagation(); + const rowItem = instance.data.rowItem; + const entry = rowItem.entries[0]; + + // Show the measure flow for measurements + OHIF.measurements.openLocationModal({ + measurement: entry, + element: document.body, + measurementApi: instance.data.measurementApi, + position: getPosition(event), + autoClick: true + }); + }, + + 'click .js-edit-description'(event, instance) { + const rowItem = instance.data.rowItem; + const entry = rowItem.entries[0]; + OHIF.ui.showDialog('measurementEditDescription', { + event, + title: 'Edit Description', + element: event.element, + measurementData: entry + }); + }, + + 'click .js-delete'(event, instance) { + event.stopPropagation(); + const dialogSettings = { + class: 'themed', + title: 'Delete measurements', + message: 'Are you sure you want to delete the measurement?', + position: getPosition(event) + }; + + OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => { + const measurementTypeId = instance.data.rowItem.measurementTypeId; + const measurement = instance.data.rowItem.entries[0]; + const measurementNumber = measurement.measurementNumber; + const { timepointApi, measurementApi } = instance.data; + + // Remove all the measurements with the given type and number + measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber }); + + // Sync the new measurement data with cornerstone tools + const baseline = timepointApi.baseline(); + measurementApi.sortMeasurements(baseline.timepointId); + + // Repaint the images on all viewports without the removed measurements + _.each($('.imageViewerViewport'), element => cornerstone.updateImage(element)); + + // Notify that viewer suffered changes + OHIF.measurements.triggerTimepointUnsavedChanges('deleted'); + }); + } +}); diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.styl b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.styl new file mode 100644 index 000000000..4e1fdf413 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableRow/measurementLightTableRow.styl @@ -0,0 +1,87 @@ +@import "{ohif:design}/app" + +.measurementLightTableRow + display: flex + margin-left: -6px + margin-top: 2px + padding-left: 6px + opacity: 0.7 + transform(scale(1)) + width: calc(100% + 6px) + + &:hover + opacity 1 + + &.active + opacity 1 + .measurementRowSidebar + theme('color', '$activeColor') + + .rowOptions + height: 35px + visibility: visible + + .rowOptions + theme('background-color', '$uiGrayDarker') + height: 0 + overflow: hidden + transition(all 0.3s ease) + visibility: hidden + padding-left: 14px + + .rowAction + theme('color', '$defaultColor') + cursor: pointer + line-height: 35px + transition(all 0.3s ease) + + &:hover, &:active + theme('color', '$textPrimaryColor') + + svg + theme('fill', '$textPrimaryColor') + theme('stroke', '$textPrimaryColor') + + svg + theme('fill', '$defaultColor') + theme('stroke', '$defaultColor') + transition(all 0.3s ease) + + &.edit-icon + width: 14px + height: 13px + + &.close-icon + width: 11px + height: 11px + + .measurementRowSidebar + theme('background', '$uiGray') + theme('color', '$textSecondaryColor') + cursor: pointer + flex: 1 + max-width: 30px + transition(all 0.3s ease) + + .measurementNumber + font-size: 14px + font-weight: 400 + margin-right: 5px + padding-top: 10px + text-align: center + + .measurementRowContent + flex: 1 + + .measurementDetails + padding: 5px 2px 0 14px + line-height: 30px + font-size: 14px + + .location + theme('color', '$textSecondaryColor') + font-weight: 400 + margin-left: -2px + + .value + theme('color', '$textPrimaryColor') diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html new file mode 100644 index 000000000..a72edffed --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.html @@ -0,0 +1,12 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.js b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.js new file mode 100644 index 000000000..a6f62f357 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.js @@ -0,0 +1,16 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { OHIF } from 'meteor/ohif:core'; + +Template.measurementLightTableView.onCreated(() => { + const instance = Template.instance(); + const { measurementApi, timepointApi } = instance.data; + + instance.data.measurementGroups = new ReactiveVar(); + + instance.autorun(() => { + measurementApi.changeObserver.depend(); + const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi); + instance.data.measurementGroups.set(data); + }); +}); diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.styl b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.styl new file mode 100644 index 000000000..606f339d3 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementLightTableView/measurementLightTableView.styl @@ -0,0 +1,13 @@ +@import "{ohif:design}/app" + +.measurementLightTableView + .report-area + theme('background-color', '$uiGrayDarker') + margin-top: 2px + padding: 10px 0 + text-align: center + + .btn + theme('background-color', '$activeColor') + theme('border', '1px solid $uiBorderColorActive') + color: #000 diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.html b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.html new file mode 100644 index 000000000..44ea10954 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.html @@ -0,0 +1,4 @@ + diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.js b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.js new file mode 100644 index 000000000..5511b2a31 --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.js @@ -0,0 +1,107 @@ +import { Meteor } from 'meteor/meteor'; +import { Template } from 'meteor/templating'; +import { Blaze } from 'meteor/blaze'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; +import { $ } from 'meteor/jquery'; +import { + segmentedTerminologyList, + segmentedTerminologyCommonList } from '../../../lib/getLabelTerminologyList'; + +Template.measurementRelabel.onCreated(() => { + const instance = Template.instance(); + + instance.value = instance.data.currentValue || ''; + + instance.state = new ReactiveVar('closed'); + + instance.items = segmentedTerminologyList; + instance.commonItems = segmentedTerminologyCommonList; +}); + +Template.measurementRelabel.onRendered(() => { + const instance = Template.instance(); + const $measurementRelabel = instance.$('.measurement-relabel'); + + // Make the measure flow bounded by the window borders + $measurementRelabel.bounded(); + + // Wait template rerender before rendering the selectTree + Tracker.afterFlush(() => { + // Get the click or rendering position + const position = { + left: event.clientX || instance.data.position.x, + top: event.clientY || instance.data.position.y, + }; + + // Define the data for selectTreeComponent + const data = { + key: 'label', + items: instance.items, + commonItems: instance.commonItems, + hideCommon: instance.data.hideCommon, + label: 'Assign label', + search: true, + searchPlaceholder: 'Search labels', + threeColumns: instance.data.threeColumns, + position + }; + + // Define in which element the selectTree will be rendered in + const parentElement = $measurementRelabel[0]; + + // Render the selectTree element + instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement); + + // Focus the measure flow to handle closing + $measurementRelabel.focus(); + }); +}); + +Template.measurementRelabel.events({ + 'click, mousedown, mouseup'(event, instance) { + event.stopPropagation(); + }, + + 'click .tree-leaf input'(event, instance) { + const $target = $(event.currentTarget); + const $measureFlow = instance.$('.measurement-relabel'); + instance.state.set('selected'); + + instance.value = $target.data('component').value(); + instance.data.updateCallback(instance.value); + + $measureFlow.trigger('close') + }, + + 'blur .measurement-relabel'(event, instance) { + const $measurementRelabel = $(event.currentTarget); + const element = $measurementRelabel[0]; + Meteor.defer(() => { + const focused = $(':focus')[0]; + if (element !== focused && !$.contains(element, focused)) { + $measurementRelabel.trigger('close'); + } + }); + }, + + 'mouseleave .measurement-relabel'(event, instance) { + const $measurementRelabel = $(event.currentTarget); + const canClose = instance.state.get() === 'selected'; + if (canClose && !$.contains($measurementRelabel[0], event.toElement)) { + $measurementRelabel.trigger('close'); + } + }, + + 'mouseenter .measurement-relabel'(event, instance) { + // Prevent from closing if user go out and in too fast + clearTimeout(instance.closingTimeout); + $(event.currentTarget).off('animationend').removeClass('fadeOut'); + }, + + 'close .measurement-relabel'(event, instance) { + // Clear the timeout to prevent executing the close process twice + clearTimeout(instance.closingTimeout); + instance.data.doneCallback(); + } +}); diff --git a/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.styl b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.styl new file mode 100644 index 000000000..f715594af --- /dev/null +++ b/Packages/ohif-measurements/client/components/measurementLightTable/measurementRelabel/measurementRelabel.styl @@ -0,0 +1,106 @@ +@import "{ohif:design}/app" + +.measurement-relabel + outline: none + position: fixed + z-index: 1 + + &.fadeOut + animateFadeOut() + + .select-tree-root>.tree-content + width: 140px + + &>.tree-options + position: relative + z-index: 2 + + .tree-inputs + position: relative + + &>.tree-leaf + position: relative + + .icon-check + theme('background-color', '$activeColor') + animateZoomIn() + left: -46px + position: absolute + top: 3px + + span + background: white + font-weight: normal + height: 46px + line-height: 46px + padding: 0 12px + + input, span + display: none + + .actions + padding-top: 16px + opacity: 0 + + &:not(.fadeOut) + animateFadeIn() + animation-delay: 0.3s + + &.fadeOut + animateFadeOut() + + &.selected>.tree-leaf + + span + display: block + + &.tree-columns + + .select-tree-root + + &.navigated .select-tree-common .content + animation-name: none + + &.selected .select-tree-common .content + animation-name: selectTreeCommonCloseLeft + + .select-tree-common + padding-left: 0 + padding-right: 6px + right: auto + left: 0 + width: 180px + + &>.tree-content + transform-origin(0% 50%) + width: 280px + + .select-tree-common .content + right: -180px + + &.started .select-tree-common .content + right: 46px + + &.navigated .select-tree-common .content + animation-name: selectTreeCommonCloseRight + + .tree-inputs>label + float: left + width: 50% + +@keyframes selectTreeCommonCloseRight { + from { + height: 37px + display: table + right: 6px + opacity: 1 + visibility: visible + } + to { + height: 100% + right: -100% + opacity: 0 + visibility: hidden + width: calc(100% - 6px) + } +} \ No newline at end of file diff --git a/Packages/ohif-measurements/client/lib/MeasurementHandlers/handleSingleMeasurementRemoved.js b/Packages/ohif-measurements/client/lib/MeasurementHandlers/handleSingleMeasurementRemoved.js index 97613994a..1208e44fe 100644 --- a/Packages/ohif-measurements/client/lib/MeasurementHandlers/handleSingleMeasurementRemoved.js +++ b/Packages/ohif-measurements/client/lib/MeasurementHandlers/handleSingleMeasurementRemoved.js @@ -12,7 +12,7 @@ export default function({ instance, eventData, tool, toolGroupId, toolGroup }) { // Stop here if the tool data shall not be persisted (e.g. temp tools) if (!Collection) return; - const measurementTypeId = measurementApi.toolsGroupsMap[measurementData.toolType]; + const measurementTypeId = measurementApi.toolsGroupsMap[eventData.toolType]; const measurement = Collection.findOne(measurementData._id); // Stop here if the measurement is already gone or never existed diff --git a/Packages/ohif-measurements/client/lib/getLabelTerminologyList.js b/Packages/ohif-measurements/client/lib/getLabelTerminologyList.js new file mode 100644 index 000000000..9fce4a88f --- /dev/null +++ b/Packages/ohif-measurements/client/lib/getLabelTerminologyList.js @@ -0,0 +1,154 @@ +const segmentedTerminologyList = [{ + label:'Abdomen/Chest Wall', + value:'Abdomen/Chest Wall', + segmentedPropCategory: 'T-D0080' + },{ + label:'Adrenal', + value:'Adrenal', + segmentedPropCategory: 'T-D0080' + },{ + label:'Bladder', + value:'Bladder', + segmentedPropCategory: 'T-D0080' + },{ + label:'Bone', + value:'Bone', + segmentedPropCategory: 'T-D0080' + },{ + label:'Brain', + value:'Brain', + segmentedPropCategory: 'T-D0080' + },{ + label:'Breast', + value:'Breast', + segmentedPropCategory: 'T-D0080' + },{ + label:'Colon', + value:'Colon', + segmentedPropCategory: 'T-D0080' + },{ + label:'Esophagus', + value:'Esophagus', + segmentedPropCategory: 'T-D0080' + },{ + label:'Extremities', + value:'Extremities', + segmentedPropCategory: 'T-D0080' + },{ + label:'Gallbladder', + value:'Gallbladder', + segmentedPropCategory: 'T-D0080' + },{ + label:'Kidney', + value:'Kidney', + segmentedPropCategory: 'T-D0080' + },{ + label:'Liver', + value:'Liver', + segmentedPropCategory: 'T-D0080' + },{ + label:'Lung', + value:'Lung', + segmentedPropCategory: 'T-D0080' + },{ + label:'Lymph Node', + value:'Lymph Node', + segmentedPropCategory: 'T-D0080' + },{ + label:'Mediastinum/Hilum', + value:'Mediastinum/Hilum', + segmentedPropCategory: 'T-D0080' + },{ + label:'Muscle', + value:'Muscle', + segmentedPropCategory: 'T-D0080' + },{ + label:'Neck', + value:'Neck', + segmentedPropCategory: 'T-D0080' + },{ + label:'Other Soft Tissue', + value:'Other Soft Tissue', + segmentedPropCategory: 'T-D0080' + },{ + label:'Ovary', + value:'Ovary', + segmentedPropCategory: 'T-D0080' + },{ + label:'Pancreas', + value:'Pancreas', + segmentedPropCategory: 'T-D0080' + },{ + label:'Pelvis', + value:'Pelvis', + segmentedPropCategory: 'T-D0080' + },{ + label:'Peritoneum/Omentum', + value:'Peritoneum/Omentum', + segmentedPropCategory: 'T-D0080' + },{ + label:'Prostate', + value:'Prostate', + segmentedPropCategory: 'T-D0080' + },{ + label:'Retroperitoneum', + value:'Retroperitoneum', + segmentedPropCategory: 'T-D0080' + },{ + label:'Small Bowel', + value:'Small Bowel', + segmentedPropCategory: 'T-D0080' + },{ + label:'Spleen', + value:'Spleen', + segmentedPropCategory: 'T-D0080' + },{ + label:'Stomach', + value:'Stomach', + segmentedPropCategory: 'T-D0080' + },{ + label:'Subcutaneous', + value:'Subcutaneous', + segmentedPropCategory: 'T-D0080' + } +]; + +const segmentedTerminologyCommonList = [{ + label:'Abdomen/Chest Wall', + value:'Abdomen/Chest Wall', + segmentedPropCategory: 'T-D0080' + },{ + label:'Liver', + value:'Liver', + segmentedPropCategory: 'T-D0080' + },{ + label:'Lung', + value:'Lung', + segmentedPropCategory: 'T-D0080' + },{ + label:'Lymph Node', + value:'Lymph Node', + segmentedPropCategory: 'T-D0080' + },{ + label:'Mediastinum/Hilum', + value:'Mediastinum/Hilum', + segmentedPropCategory: 'T-D0080' + },{ + label:'Pelvis', + value:'Pelvis', + segmentedPropCategory: 'T-D0080' + },{ + label:'Peritoneum/Omentum', + value:'Peritoneum/Omentum', + segmentedPropCategory: 'T-D0080' + },{ + label:'Retroperitoneum', + value:'Retroperitoneum', + segmentedPropCategory: 'T-D0080' + } +]; + +export { + segmentedTerminologyList, + segmentedTerminologyCommonList +} \ No newline at end of file diff --git a/Packages/ohif-measurements/client/lib/getMeasurementsGroupedByNumber.js b/Packages/ohif-measurements/client/lib/getMeasurementsGroupedByNumber.js index c0becbc9a..f8e7a630a 100644 --- a/Packages/ohif-measurements/client/lib/getMeasurementsGroupedByNumber.js +++ b/Packages/ohif-measurements/client/lib/getMeasurementsGroupedByNumber.js @@ -10,6 +10,15 @@ OHIF.measurements.getLocation = collection => { } }; +// TODO: change this to a const after refactoring newMeasurements code on measurementTableView.js +OHIF.measurements.getDescription = collection => { + for (let i = 0; i < collection.length; i++) { + if (collection[i].description) { + return collection[i].description; + } + } +}; + /** * Group all measurements by its tool group and measurement number. * @@ -76,6 +85,7 @@ OHIF.measurements.getMeasurementsGroupedByNumber = (measurementApi, timepointApi measurementTypeId: toolGroup.id, measurementNumber: key, location: OHIF.measurements.getLocation(groupObject[key]), + description: OHIF.measurements.getDescription(groupObject[key]), responseStatus: false, // TODO: Get the latest timepoint and determine the response status entries: groupObject[key] })); diff --git a/Packages/ohif-measurements/client/lib/index.js b/Packages/ohif-measurements/client/lib/index.js index d06d702d9..5b6f8386a 100644 --- a/Packages/ohif-measurements/client/lib/index.js +++ b/Packages/ohif-measurements/client/lib/index.js @@ -19,5 +19,6 @@ import './navigateOverLesions'; import './saveMeasurements'; import './syncMeasurementAndToolData'; import './toggleLabelButton'; +import './openLocationModal'; import './triggerTimepointUnsavedChanges'; import './updateMeasurementsDescription'; diff --git a/Packages/ohif-measurements/client/lib/jumpToRowItem.js b/Packages/ohif-measurements/client/lib/jumpToRowItem.js index 17b874cdf..e33d144fc 100644 --- a/Packages/ohif-measurements/client/lib/jumpToRowItem.js +++ b/Packages/ohif-measurements/client/lib/jumpToRowItem.js @@ -55,6 +55,9 @@ function renderIntoViewport(measurementData, enabledElement, viewportIndex) { function syncViewports(viewportsIndexes) { const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; + + if(!synchronizer) { return; } + const linkableViewports = synchronizer.getLinkableViewports(); if (linkableViewports.length) { const linkableViewportsIndexes = _.pluck(linkableViewports, 'index'); @@ -141,7 +144,10 @@ OHIF.measurements.jumpToRowItem = (rowItem, timepoints, childToolKey) => { const activatedViewportIndexes = []; // Deactivate stack synchronizer because it will be re-activated later - OHIF.viewer.stackImagePositionOffsetSynchronizer.deactivate(); + const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; + if(synchronizer) { + synchronizer.deactivate(); + } const renderPromises = []; for (let viewportIndex = 0; viewportIndex < numViewportsToUpdate; viewportIndex++) { diff --git a/Packages/ohif-measurements/client/lib/openLocationModal.js b/Packages/ohif-measurements/client/lib/openLocationModal.js new file mode 100644 index 000000000..9ce7a5639 --- /dev/null +++ b/Packages/ohif-measurements/client/lib/openLocationModal.js @@ -0,0 +1,61 @@ +import { Template } from 'meteor/templating'; +import { Blaze } from 'meteor/blaze'; +import { _ } from 'meteor/underscore'; +import { OHIF } from 'meteor/ohif:core'; + +OHIF.measurements.openLocationModal = options => { + let { toolType } = options.measurement; + const { tool } = OHIF.measurements.getToolConfiguration(toolType); + + if (!tool) return; + + toolType = (tool && tool.parentTool) || toolType; + + const measurementId = options.measurement._id; + let buttonView = null; + + const removeButtonView = () => { + Blaze.remove(buttonView); + buttonView = null; + }; + + if (buttonView) { + removeButtonView(); + } + + const measurementApi = options.measurementApi; + const toolCollection = measurementApi.tools[toolType]; + const measurement = toolCollection.findOne(measurementId); + + const data = { + measurement, + position: options.position, + direction: options.direction, + threeColumns: true, + hideCommon: true, + autoClick: options.autoClick, + doneCallback: removeButtonView, + updateCallback(location) { + const groupId = measurementApi.toolsGroupsMap[toolType]; + const config = OHIF.measurements.MeasurementApi.getConfiguration(); + const group = _.findWhere(config.measurementTools, { id: groupId }); + group.childTools.forEach(tool => { + measurementApi.tools[tool.id].update({ + measurementNumber: measurement.measurementNumber, + patientId: measurement.patientId + }, { + $set: { + location + } + }, { + multi: true + }); + }); + options.measurement.location = location; + + // Notify that viewer suffered changes + OHIF.measurements.triggerTimepointUnsavedChanges('relabel'); + } + }; + buttonView = Blaze.renderWithData(Template.measurementRelabel, data, document.body); +}; diff --git a/Packages/ohif-measurements/package.js b/Packages/ohif-measurements/package.js index 2adad8535..cade7ef26 100644 --- a/Packages/ohif-measurements/package.js +++ b/Packages/ohif-measurements/package.js @@ -1,5 +1,6 @@ Npm.depends({ ajv: '4.10.4', + url: '0.11.0', jspdf: '1.3.3' }); diff --git a/Packages/ohif-studies/imports/client/lib/loadStudy.js b/Packages/ohif-studies/imports/client/lib/loadStudy.js index f52461a22..c078b197a 100644 --- a/Packages/ohif-studies/imports/client/lib/loadStudy.js +++ b/Packages/ohif-studies/imports/client/lib/loadStudy.js @@ -24,6 +24,13 @@ OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => { OHIF.studies.loadingDict.set(studyInstanceUid, 'loading'); } + const studyLoaded = OHIF.viewer.Studies.findBy({ studyInstanceUid: studyInstanceUid }); + if (studyLoaded) { + OHIF.studies.loadingDict.set(studyInstanceUid, 'loaded'); + resolve(studyLoaded); + return; + } + return OHIF.studies.retrieveStudyMetadata(studyInstanceUid).then(study => { if (window.HipaaLogger && Meteor.user && Meteor.user()) { window.HipaaLogger.logEvent({ @@ -53,13 +60,9 @@ OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => { studyMetadata.addDisplaySet(displaySet); }); - // Double check to make sure this study wasn't already inserted into OHIF.viewer.Studies - // so we don't cause duplicate entry errors - const loaded = OHIF.viewer.Studies.findBy({ studyInstanceUid: study.studyInstanceUid }); - if (!loaded) { - OHIF.viewer.Studies.insert(study); - OHIF.viewer.StudyMetadataList.insert(study); - } + // Persist study data into OHIF.viewer + OHIF.viewer.Studies.insert(study); + OHIF.viewer.StudyMetadataList.insert(study); // Add the study to the loading listener to allow loading progress handling const studyLoadingListener = OHIF.viewerbase.StudyLoadingListener.getInstance(); diff --git a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js index f78ab348b..2b7b80748 100644 --- a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js +++ b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js @@ -61,9 +61,10 @@ export function setActiveViewport(element) { StudyPrefetcher.getInstance().prefetch(); // @TODO Add this to OHIFAfterActivateViewport handler... - if (OHIF.viewer.stackImagePositionOffsetSynchronizer) { - OHIF.viewer.stackImagePositionOffsetSynchronizer.update(); - } + const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; + if (!synchronizer) { return; } + + synchronizer.update(); } // Set the div to focused, so keypress events are handled diff --git a/Packages/ohif-viewerbase/client/lib/viewportUtils.js b/Packages/ohif-viewerbase/client/lib/viewportUtils.js index cac489bec..862b5110f 100644 --- a/Packages/ohif-viewerbase/client/lib/viewportUtils.js +++ b/Packages/ohif-viewerbase/client/lib/viewportUtils.js @@ -171,9 +171,7 @@ const clearTools = () => { const linkStackScroll = () => { const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; - if (!synchronizer) { - return; - } + if (!synchronizer) { return; } if (synchronizer.isActive()) { synchronizer.deactivate(); @@ -348,6 +346,9 @@ const isStackScrollLinkingActive = () => { Session.get('LayoutManagerUpdated'); const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; + + if (!synchronizer) { return; } + const syncedElements = _.pluck(synchronizer.syncedViewports, 'element'); const $renderedViewports = $('.imageViewerViewport'); $renderedViewports.each((index, element) => { diff --git a/StandaloneViewer/StandaloneViewer/.meteor/packages b/StandaloneViewer/StandaloneViewer/.meteor/packages index a80492ee3..588291bca 100644 --- a/StandaloneViewer/StandaloneViewer/.meteor/packages +++ b/StandaloneViewer/StandaloneViewer/.meteor/packages @@ -28,6 +28,7 @@ ohif:viewerbase ohif:metadata ohif:study-list ohif:dicomweb-client +ohif:measurement-table aldeed:template-extension aldeed:simple-schema@1.5.3 diff --git a/StandaloneViewer/StandaloneViewer/.meteor/versions b/StandaloneViewer/StandaloneViewer/.meteor/versions index 820ad037b..16e2e9971 100644 --- a/StandaloneViewer/StandaloneViewer/.meteor/versions +++ b/StandaloneViewer/StandaloneViewer/.meteor/versions @@ -83,11 +83,15 @@ ohif:cornerstone-settings@0.0.1 ohif:design@0.0.1 ohif:dicom-services@0.0.1 ohif:dicomweb-client@0.0.1 +ohif:hanging-protocols@0.0.1 ohif:header@0.0.1 ohif:hotkeys@0.0.1 ohif:log@0.0.1 +ohif:measurement-table@0.0.1 +ohif:measurements@0.0.1 ohif:metadata@0.0.1 ohif:polyfill@0.0.1 +ohif:select-tree@0.0.1 ohif:servers@0.0.1 ohif:studies@0.0.1 ohif:study-list@0.0.1 diff --git a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.html b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.html index 4683dbc9c..b631a9c7e 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.html +++ b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.html @@ -3,8 +3,11 @@ -
+
{{>standaloneViewerMain (clone this)}}
+
diff --git a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.js b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.js index 06206fabe..d15e0fd31 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.js +++ b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.js @@ -13,5 +13,8 @@ Template.flexboxLayout.events({ Template.flexboxLayout.helpers({ leftSidebarOpen() { return Template.instance().data.state.get('leftSidebar'); + }, + rightSidebarOpen() { + return Template.instance().data.state.get('rightSidebar'); } }); diff --git a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.styl b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.styl index b8ca4c2ed..1617f5875 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.styl +++ b/StandaloneViewer/StandaloneViewer/client/components/flexboxLayout/flexboxLayout.styl @@ -1,7 +1,5 @@ @require '{ohif:design}/app' -$lesionsSidebarMenuWidth = 450px - .viewerSection display: flex flex: 1 @@ -47,18 +45,18 @@ $lesionsSidebarMenuWidth = 450px .sidebar-right flex: 1 - margin-right: - $lesionsSidebarMenuWidth - max-width: $lesionsSidebarMenuWidth + margin-right: - $rightSidebarMenuWidth + max-width: $rightSidebarMenuWidth order: 3 position: relative &[data-timepoints="3"] - margin-right: - ($lesionsSidebarMenuWidth + 135.5px) - max-width: $lesionsSidebarMenuWidth + 135.5px + margin-right: - ($rightSidebarMenuWidth + 135.5px) + max-width: $rightSidebarMenuWidth + 135.5px &[data-timepoints="4"] - margin-right: - ($lesionsSidebarMenuWidth + 270px) - max-width: $lesionsSidebarMenuWidth + 270px + margin-right: - ($rightSidebarMenuWidth + 270px) + max-width: $rightSidebarMenuWidth + 270px &.sidebar-open margin-right: 0 diff --git a/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.html b/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.html index fb58ebd2b..067a9f914 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.html +++ b/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.html @@ -5,6 +5,9 @@ {{>roundedButtonGroup leftSidebarToggleButtonData}} {{>toolbarSectionTools toolbarButtons=toolbarButtons}} +
+ {{>roundedButtonGroup rightSidebarToggleButtonData}} +
diff --git a/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.js b/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.js index 1c0473ff1..8ab7face1 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.js +++ b/StandaloneViewer/StandaloneViewer/client/components/toolbarSection/toolbarSection.js @@ -27,6 +27,22 @@ Template.toolbarSection.helpers({ }; }, + rightSidebarToggleButtonData() { + const instance = Template.instance(); + return { + toggleable: true, + key: 'rightSidebar', + value: instance.data.state, + options: [{ + value: 'measurements', + svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-measurements-lesions', + svgWidth: 18, + svgHeight: 10, + bottomLabel: 'Measurements' + }] + }; + }, + toolbarButtons() { const extraTools = []; diff --git a/StandaloneViewer/StandaloneViewer/client/components/viewer/viewer.js b/StandaloneViewer/StandaloneViewer/client/components/viewer/viewer.js index 3238aa72b..d2e1ffa63 100644 --- a/StandaloneViewer/StandaloneViewer/client/components/viewer/viewer.js +++ b/StandaloneViewer/StandaloneViewer/client/components/viewer/viewer.js @@ -3,6 +3,8 @@ import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import { ReactiveDict } from 'meteor/reactive-dict'; import { OHIF } from 'meteor/ohif:core'; +import { MeasurementTable, measurementEvents } from 'meteor/ohif:measurement-table'; + import 'meteor/ohif:viewerbase'; import 'meteor/ohif:metadata'; @@ -48,6 +50,9 @@ const initHangingProtocol = () => { }; Template.viewer.onCreated(() => { + + OHIF.viewer.measurementTable = new MeasurementTable(); + const instance = Template.instance(); instance.state = new ReactiveDict(); @@ -98,10 +103,15 @@ Template.viewer.onCreated(() => { OHIF.viewer.StudyMetadataList.insert(studyMetadata); OHIF.viewer.data.studyInstanceUids.push(study.studyInstanceUid); }); + + // Call Viewer plugins onCreated functions + if(typeof OHIF.viewer.measurementTable.onCreated === 'function') { + OHIF.viewer.measurementTable.onCreated(instance); + } }); Template.viewer.onRendered(function() { - + const instance = Template.instance(); this.autorun(function() { // To make sure ohif viewerMain is rendered before initializing Hanging Protocols const isOHIFViewerMainRendered = Session.get('OHIFViewerMainRendered'); @@ -114,16 +124,18 @@ Template.viewer.onRendered(function() { } }); -}); - -Template.viewer.events({ - 'click .js-toggle-studies'() { - const instance = Template.instance(); - const current = instance.state.get('leftSidebar'); - instance.state.set('leftSidebar', !current); + // Call Viewer plugins onRendered functions + if(typeof OHIF.viewer.measurementTable.onRendered === 'function') { + OHIF.viewer.measurementTable.onRendered(instance); } }); +Template.viewer.events( Object.assign({ + // Viewer Events + }, + measurementEvents +)); + Template.viewer.helpers({ state() { return Template.instance().state; diff --git a/development.Dockerfile b/development.Dockerfile index 7361870eb..051f7950d 100644 --- a/development.Dockerfile +++ b/development.Dockerfile @@ -8,6 +8,7 @@ FROM node:8.10.0-slim as builder RUN apt-get update && apt-get install -y \ curl \ g++ \ + git \ build-essential RUN curl https://install.meteor.com/ | sh diff --git a/dockerfile b/dockerfile index ac961bc65..a5ce21e14 100644 --- a/dockerfile +++ b/dockerfile @@ -8,6 +8,7 @@ FROM node:8.10.0-slim as builder RUN apt-get update && apt-get install -y \ curl \ g++ \ + git \ build-essential RUN curl https://install.meteor.com/ | sh