diff --git a/.gitignore b/.gitignore index 2e15e6d39..0448892b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.vscode/ .meteor/local .meteor/meteorite .meteor/dev_bundle diff --git a/OHIFViewer/.meteor/packages b/OHIFViewer/.meteor/packages index a229e05c5..6b44cb394 100644 --- a/OHIFViewer/.meteor/packages +++ b/OHIFViewer/.meteor/packages @@ -39,6 +39,8 @@ ohif:viewerbase ohif:study-list ohif:hanging-protocols ohif:metadata +ohif:google-cloud +ohif:demo-mode ohif:user-oidc ohif:measurement-table @@ -50,4 +52,4 @@ johdirr:meteor-git-rev cultofcoders:persistent-session shell-server@0.3.1 underscore -meteortesting:mocha +meteortesting:mocha \ No newline at end of file diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions index e061f8b60..a0c2399a3 100644 --- a/OHIFViewer/.meteor/versions +++ b/OHIFViewer/.meteor/versions @@ -81,7 +81,9 @@ ohif:commands@0.0.1 ohif:core@0.0.1 ohif:cornerstone@0.0.1 ohif:cornerstone-settings@0.0.1 +ohif:demo-mode@0.0.1 ohif:design@0.0.1 +ohif:google-cloud@0.0.1 ohif:hanging-protocols@0.0.1 ohif:header@0.0.1 ohif:hotkeys@0.0.1 @@ -128,4 +130,4 @@ ui@1.0.13 underscore@1.0.10 url@1.2.0 webapp@1.6.2 -webapp-hashing@1.0.9 +webapp-hashing@1.0.9 \ No newline at end of file diff --git a/OHIFViewer/client/components/ohifViewer/ohifViewer.html b/OHIFViewer/client/components/ohifViewer/ohifViewer.html index 2750cd2db..8bce9d23d 100644 --- a/OHIFViewer/client/components/ohifViewer/ohifViewer.html +++ b/OHIFViewer/client/components/ohifViewer/ohifViewer.html @@ -23,6 +23,8 @@ {{/section}} {{/header}} - - {{>Template.dynamic template=(choose this.template 'studylist') data=this}} + + {{#if isStudyListReady}} + {{>Template.dynamic template=(choose this.template 'studylist') data=this}} + {{/if}} diff --git a/OHIFViewer/client/components/ohifViewer/ohifViewer.js b/OHIFViewer/client/components/ohifViewer/ohifViewer.js index 3e2be61ab..ce9eb71e5 100644 --- a/OHIFViewer/client/components/ohifViewer/ohifViewer.js +++ b/OHIFViewer/client/components/ohifViewer/ohifViewer.js @@ -8,6 +8,7 @@ import { OHIF } from 'meteor/ohif:core'; Template.ohifViewer.onCreated(() => { const instance = Template.instance(); instance.headerClasses = new ReactiveVar(''); + Session.set("IsStudyListReady", true);; const headerItems = [{ action: () => OHIF.ui.showDialog('serverInformationModal'), @@ -31,9 +32,11 @@ Template.ohifViewer.onCreated(() => { icon: 'fa fa-info' }]; - if (OHIF.user.userLoggedIn() === true) { + const isUserLoggedIn = OHIF.user.userLoggedIn(); + const isDemoUserLoggedIn = OHIF.demoMode && OHIF.demoMode.userLoggedIn(); + if (isUserLoggedIn || isDemoUserLoggedIn) { headerItems.push({ - action: OHIF.user.logout, + action: isDemoUserLoggedIn ? OHIF.demoMode.logout : OHIF.user.logout, text: 'Logout', iconClasses: 'logout', iconSvgUse: 'packages/ohif_viewerbase/assets/user-menu-icons.svg#logout' @@ -57,6 +60,24 @@ Template.ohifViewer.onCreated(() => { // Set the viewer open state on session Session.set('ViewerOpened', isViewer); }); + + if (OHIF.demoMode && OHIF.demoMode.userLoggedIn()) + OHIF.demoMode.setDemoServerConfig(); + else if (OHIF.gcloud && OHIF.gcloud.isEnabled()) { + const server = OHIF.servers.getCurrentServer(); + if (!server || !server.isCloud) { + Session.set("IsStudyListReady", false); + OHIF.gcloud.showDicomStorePicker({canClose: OHIF.demoMode}).then(config => { + if (!config) { + if (OHIF.demoMode) + Router.go('/demo-signin'); + return; + } + OHIF.servers.applyCloudServerConfig(config); + Session.set("IsStudyListReady", true); + }); + } + } }); Template.ohifViewer.events({ @@ -72,7 +93,8 @@ Template.ohifViewer.events({ Router.go('viewerStudies', { studyInstanceUids }); } } - } + }, + }); Template.ohifViewer.helpers({ @@ -86,5 +108,8 @@ Template.ohifViewer.helpers({ } return instance.hasViewerData ? 'Back to viewer' : ''; + }, + isStudyListReady() { + return !!Session.get('IsStudyListReady'); } }); diff --git a/OHIFViewer/client/components/ohifViewer/structuredReportModal/structureReportModal.html b/OHIFViewer/client/components/ohifViewer/structuredReportModal/structureReportModal.html new file mode 100644 index 000000000..5c52d5a15 --- /dev/null +++ b/OHIFViewer/client/components/ohifViewer/structuredReportModal/structureReportModal.html @@ -0,0 +1,11 @@ + diff --git a/OHIFViewer/client/components/ohifViewer/structuredReportModal/structuredReportModal.js b/OHIFViewer/client/components/ohifViewer/structuredReportModal/structuredReportModal.js new file mode 100644 index 000000000..90c948007 --- /dev/null +++ b/OHIFViewer/client/components/ohifViewer/structuredReportModal/structuredReportModal.js @@ -0,0 +1,91 @@ +import { $ } from 'meteor/jquery'; +import { OHIF } from 'meteor/ohif:core'; + +Template.structuredReportModal.onRendered(() => { + const structuredReport = getStructureReport(); + + render(structuredReport); +}); + +// FIXME: we use just 1st SR for current study for now +function getStructureReport() { + let structuredReport; + + OHIF.viewer.StudyMetadataList.find(studyMetadata => { + + structuredReport = studyMetadata.findInstance(instance => instance.getData().modality === 'SR'); + + // If SR is found stop the search + return !!structuredReport; + }); + + return structuredReport; +} + +function render(structureReport) { + const root = $('#root'); + + if (structureReport) { + renderStructuredReport(root, structureReport.getData()); + } else { + renderNoData(root); + } + +} + +function renderStructuredReport(root, data) { + root.append(getMainDataHtml(data)); + root.append(getContentSequenceHtml(data.contentSequence)); +} + +function renderNoData(root) { + root.append('
No structured report found
'); +} + +function getMainDataHtml(data) { + const root = $('
'); + + const { completionFlag, verificationFlag, manufacturer, contentDateTime } = data; + + if (completionFlag) { + root.append(getMainDataItemHtml('Completion flag', completionFlag)); + } + + if (verificationFlag) { + root.append(getMainDataItemHtml('Verification flag', verificationFlag)); + } + + if (manufacturer) { + root.append(getMainDataItemHtml('Manufacturer', manufacturer)); + } + + if (contentDateTime) { + root.append(getMainDataItemHtml('Content Date/Time', contentDateTime)); + } + + return root; +} + +const getContentSequenceHtml = (data, level = 1) => { + const root = $('
'); + const header = data.header; + const items = data.items || []; + + if (header) { + root.append(`${header}`); + } + + items.forEach(item => { + root.append( + item instanceof Object + ? getContentSequenceHtml(item, level + 1) + : `
${item}
` + ); + }); + + return root; +} + +function getMainDataItemHtml(key, value) { + return $(`
${key}: ${value}
`); +} \ No newline at end of file diff --git a/OHIFViewer/client/components/toolbarSection/toolbarSection.js b/OHIFViewer/client/components/toolbarSection/toolbarSection.js index 9d0eb3601..8062334fe 100644 --- a/OHIFViewer/client/components/toolbarSection/toolbarSection.js +++ b/OHIFViewer/client/components/toolbarSection/toolbarSection.js @@ -261,6 +261,22 @@ Template.toolbarSection.helpers({ buttonTemplateName: 'layoutButton' }); + buttonData.push({ + id: 'sr', + title: 'SR', + classes: 'imageViewerTool', + svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-sr' + }); + + buttonData.push({ + id: 'ps', + title: 'PS', + iconClasses: 'fa fa-th-large', + classes: 'imageViewerTool', + svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-ps' + }); + + buttonData.push({ id: 'toggleMore', title: 'More', @@ -318,4 +334,4 @@ Template.toolbarSection.onRendered(function() { } } } -}); +}); \ No newline at end of file diff --git a/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.html b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.html new file mode 100644 index 000000000..b5934b22e --- /dev/null +++ b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.html @@ -0,0 +1,25 @@ + diff --git a/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.js b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.js new file mode 100644 index 000000000..dfa2cfb39 --- /dev/null +++ b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.js @@ -0,0 +1,15 @@ +import { OHIF } from 'meteor/ohif:core'; +import { Router } from 'meteor/clinical:router'; + +Template.demoSignin.events({ + 'click #google-login-button'() { + OHIF.gcloud.setEnabled(true); + OHIF.user.login(); + }, + 'click #anonymous-login-button'() { + OHIF.gcloud.setEnabled(false); + OHIF.demoMode.login(); + Router.go('/studylist', {}, { replaceState: true }); + } +}); + diff --git a/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.styl b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.styl new file mode 100644 index 000000000..75cf61132 --- /dev/null +++ b/Packages/ohif-demo-mode/client/components/demoSignin/demoSignin.styl @@ -0,0 +1,93 @@ +@require '{ohif:design}/app' + +.demoSignin + display flex + flex 1 + flex-flow row nowrap + align-items stretch + height 100vh + min-height 600px + width 100% + background-color rgba(21, 25, 30, 0.7) + +.demo-content + margin auto + width 500px + position relative + top -150px + +.demoSigninButtons + margin auto + width 270px + font-weight 500 + +#google-login-button + display flex + justify-content center + height 40px + width 100% + color #fff + border-color #4285F4 + background #4285F4 + border-radius 3px + box-shadow 0 0 1px 0 rgba(0, 0, 0, 0.12), 0 1px 1px 0 rgba(0, 0, 0, 0.24) + cursor pointer + position relative + align-items center + +#anonymous-login-button + display flex + justify-content center + height 40px + width 100% + color #757575 + border-color white + background white + border-radius 3px + box-shadow 0 0 1px 0 rgba(0, 0, 0, 0.12), 0 1px 1px 0 rgba(0, 0, 0, 0.24) + cursor pointer + position relative + align-items center + +#google-login-button .sign-in-with-google-icon-tile + position absolute + top 1px + left 1px + background #fff + border-radius 2px + height 32px + width 32px + display flex + align-items center + justify-content center + font-weight 500 + +svg.google-icon + width 24px + height 24px + +hr.tint + border-color hsla(0, 0%, 100%, 0.3) + +.demo-brand + height 60px + display inline-block + text-decoration none + margin-bottom 30px + + .logo-image + display inline-block + fill transparent + float left + height 100% + margin 0 8px 0 0 + width 60px + margin-right 20px + + .logo-text + display inline-block + font-family Roboto, Arial, Helvetica, sans-serif + font-size 26px + font-weight 400 + theme('color', '$textPrimaryColor') + line-height 60px \ No newline at end of file diff --git a/Packages/ohif-demo-mode/client/components/index.js b/Packages/ohif-demo-mode/client/components/index.js new file mode 100644 index 000000000..16ec46a8d --- /dev/null +++ b/Packages/ohif-demo-mode/client/components/index.js @@ -0,0 +1,3 @@ +import './demoSignin/demoSignin.html'; +import './demoSignin/demoSignin.js'; +import './demoSignin/demoSignin.styl'; diff --git a/Packages/ohif-demo-mode/client/demoModeMediator.js b/Packages/ohif-demo-mode/client/demoModeMediator.js new file mode 100644 index 000000000..d4338c5df --- /dev/null +++ b/Packages/ohif-demo-mode/client/demoModeMediator.js @@ -0,0 +1,29 @@ +import { OHIF } from 'meteor/ohif:core'; +import { Router } from 'meteor/clinical:router'; +import { Servers, CurrentServer } from 'meteor/ohif:servers/both/collections'; + +const devModeMediator = {}; +export default devModeMediator; + +const DEMO_SERVER_NAME = "demo-dcm4chee"; + +devModeMediator.login = () => sessionStorage.setItem('isDemoUserSignedIn', true); + +devModeMediator.logout = () => { + if (OHIF.user.userLoggedIn()) + OHIF.user.logout(); + sessionStorage.removeItem('isDemoUserSignedIn'); + Router.go('/'); +} + +devModeMediator.userLoggedIn = () => sessionStorage.getItem('isDemoUserSignedIn'); + +devModeMediator.setDemoServerConfig = () => { + CurrentServer.remove({}); + const demoServer = Servers.findOne({ name: DEMO_SERVER_NAME }); + if (!demoServer) + throw new Error("demoServer is not found"); + CurrentServer.insert({ + serverId: demoServer._id + }); +}; \ No newline at end of file diff --git a/Packages/ohif-demo-mode/client/index.js b/Packages/ohif-demo-mode/client/index.js new file mode 100644 index 000000000..b561f9934 --- /dev/null +++ b/Packages/ohif-demo-mode/client/index.js @@ -0,0 +1,2 @@ +import './components'; +import './routes.js'; diff --git a/Packages/ohif-demo-mode/client/routes.js b/Packages/ohif-demo-mode/client/routes.js new file mode 100644 index 000000000..b449f14bc --- /dev/null +++ b/Packages/ohif-demo-mode/client/routes.js @@ -0,0 +1,26 @@ +import { Meteor } from 'meteor/meteor'; +import { Router } from 'meteor/clinical:router'; +import { OHIF } from 'meteor/ohif:core'; + +const DEMO_SIGN_IN_PAGE = '/demo-signin'; + +Router.onRun(function() { + if (!OHIF.demoMode) + this.next(); + else if (this.url === DEMO_SIGN_IN_PAGE) + this.next(); + else if (OHIF.demoMode.userLoggedIn() || OHIF.user.userLoggedIn()) { + // user is logged in whether in demo or in oidc mode + this.next(); + } else if (this.url === OHIF.user.getOidcRedirectUri()) { + // allow oidc to sign in + this.next(); + } else { + // redirect to demo login page + Router.go(DEMO_SIGN_IN_PAGE, {}, { replaceState: true }); + } +}); + +Router.route(DEMO_SIGN_IN_PAGE, function() { + this.render('demoSignin'); +}, { name: 'demo' }); \ No newline at end of file diff --git a/Packages/ohif-demo-mode/main.js b/Packages/ohif-demo-mode/main.js new file mode 100644 index 000000000..6f891fbb2 --- /dev/null +++ b/Packages/ohif-demo-mode/main.js @@ -0,0 +1,9 @@ +import { OHIF } from 'meteor/ohif:core'; +import './client'; +import devModeMediator from './client/demoModeMediator.js'; + +const demoMode = Meteor.settings && Meteor.settings.public && Meteor.settings.public.demoMode; + +if (demoMode) { + OHIF.demoMode = devModeMediator; +} \ No newline at end of file diff --git a/Packages/ohif-demo-mode/package.js b/Packages/ohif-demo-mode/package.js new file mode 100644 index 000000000..b9013a37b --- /dev/null +++ b/Packages/ohif-demo-mode/package.js @@ -0,0 +1,20 @@ +Package.describe({ + name: 'ohif:demo-mode', + summary: 'demo mode', + version: '0.0.1', +}); + +Package.onUse(function(api) { + api.versionsFrom('1.4'); + + api.use('http'); + api.use('ecmascript'); + api.use(['templating', 'stylus'], 'client'); + + // OHIF dependencies + api.use('ohif:core', 'client'); + + // Main module + api.mainModule('main.js', 'client'); + +}); diff --git a/Packages/ohif-google-cloud/.meteorignore b/Packages/ohif-google-cloud/.meteorignore new file mode 100644 index 000000000..8cd000bc2 --- /dev/null +++ b/Packages/ohif-google-cloud/.meteorignore @@ -0,0 +1,3 @@ +healthcare-api-adapter/node_modules/ +healthcare-api-adapter/public/ +healthcare-api-adapter/src/ \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.html b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.html new file mode 100644 index 000000000..98bd592c2 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.html @@ -0,0 +1,12 @@ + diff --git a/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.js b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.js new file mode 100644 index 000000000..180160a6c --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; +import { Template } from 'meteor/templating'; +import { OHIF } from 'meteor/ohif:core'; + +Template.gcloudDialog.onRendered(() => { + const instance = Template.instance(); + + // Allow options ovewrite + const modalOptions = _.extend( + { + backdrop: 'static', + keyboard: false, + width: 650, + }, + instance.data.modalOptions + ); + + const $modal = instance.$('.modal'); + + // Create the bootstrap modal + $modal.modal(modalOptions); +}); diff --git a/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.styl b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.styl new file mode 100644 index 000000000..ae7de0a87 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dialogs/gcloudDialog.styl @@ -0,0 +1,6 @@ +@import '{ohif:design}/app' + +.gcloud-dialog + display flex + .modal-dialog + margin auto \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.html b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.html new file mode 100644 index 000000000..ba23a4016 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.js b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.js new file mode 100644 index 000000000..57f12070c --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.js @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; + +const DATASET_PICKER_ID = 'gcp-dataset-picker'; +const EVENT_NAME = 'onSelect'; + +Template.dicomStorePicker.onRendered(() => { + const instance = Template.instance(); + instance.$('#' + DATASET_PICKER_ID).on(EVENT_NAME, (event, data) => { + instance + .$('.modal') + .one('hidden.bs.modal', event => { + instance.data.promiseResolve(data); + }) + .modal('hide'); + }); +}); + +Template.dicomStorePicker.helpers({ + datasetPickerId() { + return DATASET_PICKER_ID; + }, + eventName() { + return EVENT_NAME; + }, + oidcStorageKey() { + return OHIF.user.getOidcStorageKey(); + }, +}); diff --git a/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.styl b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.styl new file mode 100644 index 000000000..f669618c4 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/dicomStorePicker/dicomStorePicker.styl @@ -0,0 +1,5 @@ +@import '{ohif:design}/app' + +#dicomStorePicker + .modal-dialog + width 694px \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/index.js b/Packages/ohif-google-cloud/client/components/index.js new file mode 100644 index 000000000..25d25ebe4 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/index.js @@ -0,0 +1,12 @@ +import './dialogs/gcloudDialog.html'; +import './dialogs/gcloudDialog.js'; +import './dialogs/gcloudDialog.styl'; + + +import './dicomStorePicker/dicomStorePicker.html'; +import './dicomStorePicker/dicomStorePicker.js'; +import './dicomStorePicker/dicomStorePicker.styl'; + +import './uploadStudiesDialog/uploadStudiesDialog.html'; +import './uploadStudiesDialog/uploadStudiesDialog.js'; +import './uploadStudiesDialog/uploadStudiesDialog.styl'; \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.html b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.html new file mode 100644 index 000000000..7373c1c7f --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.js b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.js new file mode 100644 index 000000000..110f428cc --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.js @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor'; + +const DICOM_FILE_UPLOADER_ID = 'gcp-dicom-uploader'; +const EVENT_NAME = 'onClose'; + +Template.uploadStudiesDialog.onRendered(() => { + const instance = Template.instance(); + instance.$('#' + DICOM_FILE_UPLOADER_ID).on(EVENT_NAME, (event, data) => { + instance + .$('.modal') + .one('hidden.bs.modal', event => { + instance.data.promiseResolve(data); + }) + .modal('hide'); + }); +}); + +Template.uploadStudiesDialog.helpers({ + dicomFilesUploaderId() { + return DICOM_FILE_UPLOADER_ID; + }, + eventName() { + return EVENT_NAME; + }, + oidcStorageKey() { + return OHIF.user.getOidcStorageKey(); + }, + url() { + return OHIF.gcloud.getConfig().qidoRoot; // FIXME: not QIDO + } +}); diff --git a/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.styl b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.styl new file mode 100644 index 000000000..ea3005d96 --- /dev/null +++ b/Packages/ohif-google-cloud/client/components/uploadStudiesDialog/uploadStudiesDialog.styl @@ -0,0 +1,8 @@ +@import '{ohif:design}/app' + +#dicomFilesUploader + .modal-dialog + .modal-body + padding 0 + width 536px + max-height 563px \ No newline at end of file diff --git a/Packages/ohif-google-cloud/client/index.js b/Packages/ohif-google-cloud/client/index.js new file mode 100644 index 000000000..095e70eb8 --- /dev/null +++ b/Packages/ohif-google-cloud/client/index.js @@ -0,0 +1,8 @@ +import { loadScript } from "../lib/utils" + +loadScript("/packages/ohif_google-cloud/.npm/package/node_modules/healthcare-api-adapter/dist/vue.js", () => { + loadScript("/packages/ohif_google-cloud/.npm/package/node_modules/healthcare-api-adapter/dist/gcp.min.js"); +}); + + +import './components'; diff --git a/Packages/ohif-google-cloud/lib/GCloudAdapter.js b/Packages/ohif-google-cloud/lib/GCloudAdapter.js new file mode 100644 index 000000000..17aab1a38 --- /dev/null +++ b/Packages/ohif-google-cloud/lib/GCloudAdapter.js @@ -0,0 +1,41 @@ +const GCloudAdapter = {}; + +const GCP_HEALTHCARE_CONFIG = 'GCP_HEALTHCARE_CONFIG'; +let isEnabled = true; + +GCloudAdapter.getConfig = function() { + const configStr = sessionStorage.getItem(GCP_HEALTHCARE_CONFIG); + if (configStr) return JSON.parse(configStr); + return null; +}; + +GCloudAdapter.setConfig = function(config) { + if (config) sessionStorage.setItem(GCP_HEALTHCARE_CONFIG, JSON.stringify(config)); + else sessionStorage.removeItem(GCP_HEALTHCARE_CONFIG); +}; + +GCloudAdapter.showDicomStorePicker = function(options) { + return OHIF.ui.showDialog('dicomStorePicker', options).then(config => { + if (config) OHIF.gcloud.setConfig(config); + return config; + }); +}; + +GCloudAdapter.showUploadStudiesDialog = function() { + return OHIF.ui.showDialog('uploadStudiesDialog') +}; + +const gcpConfig = GCloudAdapter.getConfig(); +if (gcpConfig) { + OHIF.servers.applyCloudServerConfig(gcpConfig); +} + +GCloudAdapter.setEnabled = function(value) { + isEnabled = value; +} + +GCloudAdapter.isEnabled = function() { + return isEnabled; +}; + +export default GCloudAdapter; diff --git a/Packages/ohif-google-cloud/lib/utils.js b/Packages/ohif-google-cloud/lib/utils.js new file mode 100644 index 000000000..a6084eb99 --- /dev/null +++ b/Packages/ohif-google-cloud/lib/utils.js @@ -0,0 +1,10 @@ +export function loadScript(url, callback=undefined) +{ + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.onreadystatechange = callback; + script.onload = callback; + head.appendChild(script); +} \ No newline at end of file diff --git a/Packages/ohif-google-cloud/main.js b/Packages/ohif-google-cloud/main.js new file mode 100644 index 000000000..937a20b37 --- /dev/null +++ b/Packages/ohif-google-cloud/main.js @@ -0,0 +1,9 @@ +import { OHIF } from 'meteor/ohif:core'; +import GCloudAdapter from './lib/GCloudAdapter'; + +if (Meteor.settings.public.googleCloud) { + const gcloud = GCloudAdapter; + OHIF.gcloud = gcloud; +} + + diff --git a/Packages/ohif-google-cloud/package.js b/Packages/ohif-google-cloud/package.js new file mode 100644 index 000000000..3a3b34177 --- /dev/null +++ b/Packages/ohif-google-cloud/package.js @@ -0,0 +1,44 @@ +Package.describe({ + name: 'ohif:google-cloud', + summary: 'DICOM Services: Google Cloud Healthcare API integration', + version: '0.0.1', + documentation: 'README.md', +}); + +Npm.depends({ + 'healthcare-api-adapter': "git+https://github.com/quantumsoftgroup/healthcare-api-adapter#v0.2.1" +}); + + +Package.onUse(function(api) { + api.versionsFrom('1.4'); + + api.use('http'); + api.use('ecmascript'); + + api.use(['templating', 'stylus'], 'client'); + + // Main module + api.mainModule('main.js', ['client']); + + // Client imports and routes + api.addFiles('client/index.js', 'client'); + + var assets = [ + '.npm/package/node_modules/healthcare-api-adapter/dist/gcp.min.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/gcp.0.min.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/gcp.2.min.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/gcp.3.min.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/gcp.4.min.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/vue.js', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/Button_File.473e74a7.svg', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/Button_Folder.271da60b.svg', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/Icon-24px-Close.d1a4d6d2.svg', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/Icon-Arrow.e493b444.svg', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/Icon-Warn.f3b4b640.svg', + '.npm/package/node_modules/healthcare-api-adapter/dist/img/arrow_right.d8a5b209.svg', + ]; + + api.addAssets(assets, 'client'); +}); + diff --git a/Packages/ohif-measurement-table/client/index.js b/Packages/ohif-measurement-table/client/index.js index bf641764e..6e8095097 100644 --- a/Packages/ohif-measurement-table/client/index.js +++ b/Packages/ohif-measurement-table/client/index.js @@ -31,21 +31,29 @@ class MeasurementTable { Session.set('TimepointsReady', true); await measurementApi.retrieveMeasurements(patientId, [OHIF.viewer.data.currentTimepointId]); - Session.set('MeasurementsReady', true); + Session.set('MeasurementsReady', false); measurementApi.syncMeasurementsAndToolData(); + this.jumpToFirstMeasurement(); + const viewportUtils = OHIF.viewerbase.viewportUtils; this.firstMeasurementActivated = false; - + this.dataIsavalible = false; instance.autorun(() => { if (!Session.get('TimepointsReady') || !Session.get('MeasurementsReady') || !Session.get('ViewerReady') || this.firstMeasurementActivated) { + if (this.dataIsavalible) { + viewportUtils.hideTools(); + this.dataIsavalible = false; + } return; } - - this.jumpToFirstMeasurement(); + if(!this.dataIsavalible){ + viewportUtils.unhideTools(); + this.dataIsavalible = true; + } }); diff --git a/Packages/ohif-measurement-table/client/schema/length.js b/Packages/ohif-measurement-table/client/schema/length.js index 20c17a98d..46ffa90ea 100644 --- a/Packages/ohif-measurement-table/client/schema/length.js +++ b/Packages/ohif-measurement-table/client/schema/length.js @@ -47,6 +47,11 @@ const toolSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasu label: 'Length', optional: true, decimal: true + }, + dashed: { + type: String, + label: 'dashed', + optional: true } }]); diff --git a/Packages/ohif-measurement-table/client/utils/srUtils.js b/Packages/ohif-measurement-table/client/utils/srUtils.js index 8805993b6..2d2462059 100644 --- a/Packages/ohif-measurement-table/client/utils/srUtils.js +++ b/Packages/ohif-measurement-table/client/utils/srUtils.js @@ -1,6 +1,16 @@ import { dcmjs } from 'meteor/ohif:cornerstone'; -const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22']; +const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22', '1.2.840.10008.5.1.4.1.1.11.1']; + +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(); @@ -59,5 +69,5 @@ const getLatestSRSeries = () => { export { getAllDisplaySets, getInstanceMetadata, - getLatestSRSeries, -} + getLatestSRSeries +} \ No newline at end of file diff --git a/Packages/ohif-servers/both/lib/applyCloudServerConfig.js b/Packages/ohif-servers/both/lib/applyCloudServerConfig.js new file mode 100644 index 000000000..8341c5257 --- /dev/null +++ b/Packages/ohif-servers/both/lib/applyCloudServerConfig.js @@ -0,0 +1,28 @@ +import { OHIF } from 'meteor/ohif:core'; +import { Session } from 'meteor/session'; +import { Servers, CurrentServer } from 'meteor/ohif:servers/both/collections'; + +/** + * Recreates a current server with GCloud config + */ +OHIF.servers.applyCloudServerConfig = (config) => { + Session.set('GCP_HEALTHCARE_CONFIG', config); + CurrentServer.remove({}); + if (!config) + return; + config.name = "gcs"; + config.imageRendering = "wadors"; + config.origin = "json"; + config.thumbnailRendering = "wadors"; + config.qidoSupportsIncludeField = false; + config.type = "dicomweb"; + config.requestOptions = {}; + config.requestOptions.requestFromBrowser = true; + config.origin = 'json'; + config.type = 'dicomWeb'; + config.isCloud = true; + const serverId = Servers.insert(config); + CurrentServer.insert({ + serverId + }); +}; \ No newline at end of file diff --git a/Packages/ohif-servers/both/lib/index.js b/Packages/ohif-servers/both/lib/index.js index 0df54741b..73ccf0759 100644 --- a/Packages/ohif-servers/both/lib/index.js +++ b/Packages/ohif-servers/both/lib/index.js @@ -1 +1,2 @@ import './getCurrentServer.js'; +import './applyCloudServerConfig.js'; diff --git a/Packages/ohif-studies/imports/both/services/index.js b/Packages/ohif-studies/imports/both/services/index.js index 8808a8ee6..11aa91bb9 100644 --- a/Packages/ohif-studies/imports/both/services/index.js +++ b/Packages/ohif-studies/imports/both/services/index.js @@ -3,4 +3,5 @@ import './namespace'; // DICOMWeb instance, study, and metadata retrieval import './qido/instances.js'; import './qido/studies.js'; +import './qido/retrieveMetadata.js'; import './wado/retrieveMetadata.js'; diff --git a/Packages/ohif-studies/imports/both/services/qido/retrieveMetadata.js b/Packages/ohif-studies/imports/both/services/qido/retrieveMetadata.js new file mode 100644 index 000000000..71b29ee05 --- /dev/null +++ b/Packages/ohif-studies/imports/both/services/qido/retrieveMetadata.js @@ -0,0 +1,110 @@ + +import { OHIF } from 'meteor/ohif:core'; +import DICOMwebClient from 'dicomweb-client'; +const { DICOMWeb } = OHIF; + +/** + * Creates a URL for a WADO search + * + * @param server + * @param studyInstanceUid + * @returns {string} + */ +function buildUrl(server, studyInstanceUid) { + return server.wadoRoot + '/studies?includefield=all&StudyInstanceUID=' + studyInstanceUid; +} + +function buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid) { + return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}` +} + +function buildInstanceFrameWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) { + const baseWadoRsUri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + frame = frame != null || 1; + + return `${baseWadoRsUri}/frames/${frame}` +} +/** + * Parses result data from a QIDO search into Study MetaData + * Returns an object populated with study metadata, including the + * series list. + * + * @param server + * @param studyInstanceUid + * @param resultData + * @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}} + */ +async function resultDataToStudyMetadata(server, studyInstanceUid, resultData, instancesIn) { + + const seriesList = []; + + if (!resultData.length) { + return; + } + + const anInstance = resultData[0]; + if (!anInstance) { + return; + } + + const studyData = { + seriesList, + patientName: DICOMWeb.getName(anInstance['00100010']), + patientId: DICOMWeb.getString(anInstance['00100020']), + patientAge: DICOMWeb.getNumber(anInstance['00101010']), + patientSize: DICOMWeb.getNumber(anInstance['00101020']), + patientWeight: DICOMWeb.getNumber(anInstance['00101030']), + accessionNumber: DICOMWeb.getString(anInstance['00080050']), + studyDate: DICOMWeb.getString(anInstance['00080020']), + modalities: DICOMWeb.getString(anInstance['00080061']), + studyDescription: DICOMWeb.getString(anInstance['00081030']), + imageCount: DICOMWeb.getString(anInstance['00201208']), + studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']), + institutionName: DICOMWeb.getString(anInstance['00080080']) + }; + await Promise.all(instancesIn.seriesList.map(async function(seriesMap) { + var instance = seriesMap.instances[0]; + var seriesInstanceUid = instance.seriesInstanceUid; + var series = seriesMap[seriesInstanceUid]; + if (!series) { + series = seriesMap; + series.instances = []; + seriesMap[seriesInstanceUid] = series; + seriesList.push(series); + } + const sopInstanceUid = instance.sopInstanceUid; + const wadouri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + const baseWadoRsUri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + const wadorsuri = buildInstanceFrameWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + const instanceSummary = instance; + instanceSummary.baseWadoRsUri=baseWadoRsUri; + instanceSummary.wadouri=WADOProxy.convertURL(wadouri, server); + instanceSummary.wadorsuri=WADOProxy.convertURL(wadorsuri, server); + instanceSummary.imageRendering=server.imageRendering; + instanceSummary.thumbnailRendering=server.thumbnailRendering; + series.instances.push(instanceSummary); + })); + return studyData; +} + +/** + * Retrieved Study MetaData from a DICOM server using a WADO call + * @param server + * @param studyInstanceUid + * @returns {Promise} + */ +OHIF.studies.services.QIDO.RetrieveMetadata = async function(server, studyInstanceUid) { + const url = buildUrl(server, studyInstanceUid); + return new Promise((resolve, reject) => { + DICOMWeb.getJSON(url, server.requestOptions).then(result => { + OHIF.studies.services.QIDO.Instances(server, studyInstanceUid).then(instances => { + resultDataToStudyMetadata(server, studyInstanceUid, result, instances).then((study) => { + study.wadoUriRoot = server.wadoUriRoot; + study.studyInstanceUid = studyInstanceUid; + resolve(study); + }, reject) + }, reject); + }, reject); + }); +}; diff --git a/Packages/ohif-studies/imports/both/services/qido/studies.js b/Packages/ohif-studies/imports/both/services/qido/studies.js index 830bd9302..1d1be4501 100644 --- a/Packages/ohif-studies/imports/both/services/qido/studies.js +++ b/Packages/ohif-studies/imports/both/services/qido/studies.js @@ -38,8 +38,8 @@ function dateToString(date) { */ function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) { const commaSeparatedFields = [ - '00081030', // Study Description - '00080060' //Modality + // this is temp '00081030', // Study Description + // '00080060' //Modality // Add more fields here if you want them in the result ].join(','); diff --git a/Packages/ohif-studies/imports/both/services/wado/retrieveMetadata.js b/Packages/ohif-studies/imports/both/services/wado/retrieveMetadata.js index a4c9f8904..9f0a228a5 100644 --- a/Packages/ohif-studies/imports/both/services/wado/retrieveMetadata.js +++ b/Packages/ohif-studies/imports/both/services/wado/retrieveMetadata.js @@ -214,6 +214,112 @@ function getRadiopharmaceuticalInfo(instance) { }; } +function getRelationshipString (data) { + const relationshipType = DICOMWeb.getString(data['0040A010']); + + switch (relationshipType) { + case 'HAS CONCEPT MOD': + return 'Concept modifier: '; + case 'HAS OBS CONTEXT': + return 'Observation context: '; + default: + return ''; + } +} + +const getNestedObject = (data) => data.Value[0] || {}; + +const getMeaningString = (data) => (data['0040A043'] && `${DICOMWeb.getString(data['0040A043'].Value[0]['00080104'])} = `) || ''; + +function getValueString (data) { + switch (DICOMWeb.getString(data['0040A040'])) { // ValueType + case 'CODE': + const conceptCode = getNestedObject(data['0040A168']); + const conceptCodeValue = DICOMWeb.getString(conceptCode['00080100']); + const conceptCodeMeaning = DICOMWeb.getString(conceptCode['00080104']); + const schemeDesignator = DICOMWeb.getString(conceptCode['00080102']); + return `${conceptCodeMeaning} (${conceptCodeValue}, ${schemeDesignator})`; + + case 'PNAME': + return DICOMWeb.getName(data['0040A123']); + + case 'TEXT': + return DICOMWeb.getString(data['0040A160']); + + case 'UIDREF': + return DICOMWeb.getString(data['0040A124']); + + case 'NUM': + const numValue = DICOMWeb.getString(getNestedObject(data['0040A300'])['0040A30A']); + const codeValue = DICOMWeb.getString(getNestedObject(getNestedObject(data['0040A300'])['004008EA'])['00080100']); + return `${numValue} ${codeValue}`; + } +} + +function constructPlainValue(data) { + const value = getValueString(data); + + if (value) { + return getRelationshipString(data) + getMeaningString(data) + value; + } +} + +function constructContentSequence(data, header) { + if (!data['0040A730'].Value) { + return; + } + + const items = data['0040A730'].Value.map(item => parseContent(item)).filter(item => item); + + if (items.length) { + const result = { + items + }; + + if (header) { + result.header = header; + } + + return result; + } +} + +/** + * Recursively parses content sequence for structured reports + * + * @param instance The instance + * @returns {Array} Series List + */ + +function parseContent(instance) { + if (instance['0040A040']) { // ValueType + if (DICOMWeb.getString(instance['0040A040']) === 'CONTAINER') { + const header = DICOMWeb.getString(getNestedObject(instance['0040A043'])['00080104']); // TODO: check with real data + return constructContentSequence(instance, header); + } + + return constructPlainValue(instance); + } + + if (instance['0040A730']) { //ContentSequence + return constructContentSequence(instance); + } +} + +function getModality(instance) { + const modality = DICOMWeb.getString(instance['00080060']); + return modality || (!!instance['0040A730'] && 'SR') || undefined; // FIXME: dirty, dirty hack, we use +} + +function getContentDateTime(instance) { + const date = DICOMWeb.getString(instance['00080023']); + const time = DICOMWeb.getString(instance['00080033']); + + if (date && time) { + return `${date.substr(0, 4)}-${date.substr(4, 2)}-${date.substr(6, 2)} ${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}`; + } +} + /** * Parses result data from a WADO search into Study MetaData * Returns an object populated with study metadata, including the @@ -257,11 +363,12 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { await Promise.all(resultData.map(async function(instance) { const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']); let series = seriesMap[seriesInstanceUid]; + const modality = getModality(instance); if (!series) { series = { seriesDescription: DICOMWeb.getString(instance['0008103E']), - modality: DICOMWeb.getString(instance['00080060']), + modality, seriesInstanceUid: seriesInstanceUid, seriesNumber: DICOMWeb.getNumber(instance['00200011']), seriesDate: DICOMWeb.getString(instance['00080021']), @@ -276,11 +383,15 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { const wadouri = buildInstanceWadoUrl(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); const baseWadoRsUri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); const wadorsuri = buildInstanceFrameWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid); - const instanceSummary = { + contentSequence: parseContent(instance), + completionFlag: DICOMWeb.getString(instance['0040A491']), + manufacturer: DICOMWeb.getString(instance['00080070']), + verificationFlag: DICOMWeb.getString(instance['0040A493']), + contentDateTime: getContentDateTime(instance), imageType: DICOMWeb.getString(instance['00080008']), sopClassUid: DICOMWeb.getString(instance['00080016']), - modality: DICOMWeb.getString(instance['00080060']), + modality, sopInstanceUid, instanceNumber: DICOMWeb.getNumber(instance['00200013']), imagePositionPatient: DICOMWeb.getString(instance['00200032']), @@ -322,8 +433,8 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { contrastBolusAgent: DICOMWeb.getString(instance['00180010']), radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance), baseWadoRsUri: baseWadoRsUri, - wadouri: WADOProxy.convertURL(wadouri, server), - wadorsuri: WADOProxy.convertURL(wadorsuri, server), + wadouri: wadouri, + wadorsuri: wadorsuri, imageRendering: server.imageRendering, thumbnailRendering: server.thumbnailRendering }; @@ -351,7 +462,6 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { series.instances.push(instanceSummary); })); - return studyData; } diff --git a/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js b/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js index 79c99afdb..b8a503c93 100644 --- a/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js +++ b/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js @@ -49,9 +49,15 @@ OHIF.studies.retrieveStudyMetadata = (studyInstanceUid, seriesInstanceUids) => { // If no study metadata is in the cache variable, we need to retrieve it from // the server with a call. if (server.type === 'dicomWeb' && server.requestOptions.requestFromBrowser === true) { - OHIF.studies.services.WADO.RetrieveMetadata(server, studyInstanceUid).then(function (data) { - resolve(data) - }, reject); + if(server.metadataSource ==='qido') { + OHIF.studies.services.QIDO.RetrieveMetadata(server, studyInstanceUid).then(function (data) { + resolve(data) + }, reject); + } else { + OHIF.studies.services.WADO.RetrieveMetadata(server, studyInstanceUid).then(function (data) { + resolve(data) + }, reject); + } } else { Meteor.call('GetStudyMetadata', studyInstanceUid, function (error, study) { OHIF.log.timeEnd(timingKey); diff --git a/Packages/ohif-studies/imports/client/lib/searchStudies.js b/Packages/ohif-studies/imports/client/lib/searchStudies.js index 8fe6b6949..723109a02 100644 --- a/Packages/ohif-studies/imports/client/lib/searchStudies.js +++ b/Packages/ohif-studies/imports/client/lib/searchStudies.js @@ -10,13 +10,12 @@ const studySearchPromises = new Map(); * @returns {Promise} resolved with an array of studies information or rejected with an error */ OHIF.studies.searchStudies = filter => { - const promiseKey = JSON.stringify(filter); + const server = OHIF.servers.getCurrentServer(); + const promiseKey = server.qidoRoot + " - " + JSON.stringify(filter); if (studySearchPromises.has(promiseKey)) { return studySearchPromises.get(promiseKey); } else { const promise = new Promise((resolve, reject) => { - const server = OHIF.servers.getCurrentServer(); - if (server.type === 'dicomWeb' && server.requestOptions.requestFromBrowser === true) { OHIF.studies.services.QIDO.Studies(server, filter).then(resolve, reject); } else { diff --git a/Packages/ohif-studies/imports/server/methods/getStudyMetadata.js b/Packages/ohif-studies/imports/server/methods/getStudyMetadata.js index b97d04316..e22323453 100644 --- a/Packages/ohif-studies/imports/server/methods/getStudyMetadata.js +++ b/Packages/ohif-studies/imports/server/methods/getStudyMetadata.js @@ -12,14 +12,18 @@ Meteor.methods({ // Get the server data. This is user-defined in the config.json files or through servers // configuration modal const server = OHIF.servers.getCurrentServer(); - if (!server) { throw new Meteor.Error('improper-server-config', 'No properly configured server was available over DICOMWeb or DIMSE.'); } try { if (server.type === 'dicomWeb') { - return OHIF.studies.services.WADO.RetrieveMetadata(server, studyInstanceUid); + if(server.metadataSource ==='qido') { + return OHIF.studies.services.QIDO.RetrieveMetadata(server, studyInstanceUid); + } + else{ + return OHIF.studies.services.WADO.RetrieveMetadata(server, studyInstanceUid); + } } else if (server.type === 'dimse') { return OHIF.studies.services.DIMSE.RetrieveMetadata(studyInstanceUid); } diff --git a/Packages/ohif-study-list/client/components/studylist/studylistResult/studylistResult.html b/Packages/ohif-study-list/client/components/studylist/studylistResult/studylistResult.html index c6ebb2f6b..c5c2600f7 100644 --- a/Packages/ohif-study-list/client/components/studylist/studylistResult/studylistResult.html +++ b/Packages/ohif-study-list/client/components/studylist/studylistResult/studylistResult.html @@ -1,6 +1,9 @@ diff --git a/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.js b/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.js index a68b49885..dc0ee6f73 100644 --- a/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.js +++ b/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.js @@ -1,5 +1,6 @@ import { OHIF } from 'meteor/ohif:core'; import { Template } from 'meteor/templating'; +import { Router } from 'meteor/clinical:router'; import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; import { $ } from 'meteor/jquery'; @@ -25,6 +26,21 @@ Template.studylistToolbar.events({ 'click .js-import-files'(event) { // Reset file input $(event.currentTarget).val(''); + }, + + 'click .uploadStudiesBtn'() { + OHIF.gcloud.showUploadStudiesDialog(); + }, + + 'click .changeDicomStoreBtn'() { + OHIF.gcloud.showDicomStorePicker({canClose:true}).then(config => { + if (!config) + return; + Session.set("IsStudyListReady", false); + OHIF.studylist.collections.Studies.remove({}); + OHIF.servers.applyCloudServerConfig(config); + setImmediate(() => Session.set("IsStudyListReady", true)); + }); } }); @@ -32,5 +48,11 @@ Template.studylistToolbar.helpers({ importSupported() { const importSupported = Session.get('importSupported'); return (importSupported && OHIF.uiSettings.studyListFunctionsEnabled); + }, + uploadSupported() { + return !!(OHIF.gcloud && OHIF.gcloud.isEnabled()); + }, + changeDicomStoreSupported() { + return !!(OHIF.gcloud && OHIF.gcloud.isEnabled()); } }); diff --git a/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.styl b/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.styl index 23944cbad..6c464b475 100644 --- a/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.styl +++ b/Packages/ohif-study-list/client/components/studylist/studylistToolbar/studylistToolbar.styl @@ -25,3 +25,6 @@ overflow: hidden position: absolute z-index: -1 + .uploadStudiesBtn + margin 0 20px + diff --git a/Packages/ohif-user-oidc/imports/client/oidcUserManager.js b/Packages/ohif-user-oidc/imports/client/oidcUserManager.js index e3f73887d..6f409cb18 100644 --- a/Packages/ohif-user-oidc/imports/client/oidcUserManager.js +++ b/Packages/ohif-user-oidc/imports/client/oidcUserManager.js @@ -16,20 +16,29 @@ const oidcClient = oidc[0]; const redirect_uri = Meteor.absoluteUrl(oidcClient.authRedirectUri); const silent_redirect_uri = Meteor.absoluteUrl('/packages/ohif_user-oidc/public/silent-refresh.html'); +function httpGetSync(theUrl) +{ + // TODO: consider making it async somehow + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open( "GET", theUrl, false ); // false for synchronous request + xmlHttp.send( null ); + return xmlHttp.responseText; +} +const id = oidcClient.clientId || httpGetSync( Meteor.absoluteUrl('/gcloud-client-id')); const settings = { authority: oidcClient.authServerUrl, - client_id: oidcClient.clientId, + client_id: id, redirect_uri, silent_redirect_uri, post_logout_redirect_uri: Meteor.absoluteUrl(oidcClient.postLogoutRedirectUri), response_type: oidcClient.responseType || 'id_token token', - scope: oidc.scope || 'email profile openid', // Note: Request must have scope 'openid' to be considered an OpenID Connect request + scope: oidcClient.scope || 'email profile openid https://www.googleapis.com/auth/cloud-platform.read-only https://www.googleapis.com/auth/cloud-healthcare', // seems like scope is not loaded from settings Note: Request must have scope 'openid' to be considered an OpenID Connect request automaticSilentRenew: true, revokeAccessTokenOnSignout: true, }; -const itemName = `oidc.user:${oidcClient.authServerUrl}:${oidcClient.clientId}`; +const itemName = `oidc.user:${oidcClient.authServerUrl}:${id}`; function getTokenFromStorage() { const userDataJSON = sessionStorage.getItem(itemName); @@ -46,12 +55,34 @@ OHIF.user.getAccessToken = function oidcGetAccessToken() { if (!OHIF.user.userLoggedIn) { throw new Error('User is not logged in.'); } - return getTokenFromStorage(); }; +OHIF.user.getOidcStorageKey = function () { + return itemName; +} + +OHIF.user.getOidcRedirectUri = function () { + return oidcClient.authRedirectUri; +} + +OHIF.user.login = function oidcLogin() { + oidcUserManager.signinRedirect({redirect_uri}); +} + OHIF.user.logout = function oidcLogout() { - oidcUserManager.signoutRedirect(); + const config = JSON.parse(sessionStorage.getItem(itemName) || null); + if (oidcClient.revokeUrl && config && config.access_token) { + // OIDC from Google doesn't support signing out for some reason + // so we revoke the token manually + sessionStorage.removeItem(itemName); + const revokeUrl = oidcClient.revokeUrl + config.access_token; + fetch(revokeUrl).catch(()=>{}).then(() => location.assign(oidcClient.postLogoutRedirectUri || '/')); + + } else { + // simple oidc signout behavior + oidcUserManager.signoutRedirect(); + } } OHIF.user.userLoggedIn = () => !!getTokenFromStorage(); @@ -70,7 +101,7 @@ function handleSilentRenewError(error) { } oidcUserManager.events.addSilentRenewError(handleSilentRenewError); - +oidcUserManager.startSilentRenew(); oidcUserManager.events.addAccessTokenExpired(function(){ OHIF.user.logout(); }); diff --git a/Packages/ohif-user-oidc/imports/client/routes.js b/Packages/ohif-user-oidc/imports/client/routes.js index 19652efba..7c702cb5d 100644 --- a/Packages/ohif-user-oidc/imports/client/routes.js +++ b/Packages/ohif-user-oidc/imports/client/routes.js @@ -66,14 +66,21 @@ function urlHasSignInResponse() { } Router.onRun(function() { - const next = this.next; - const redirect_uri = Meteor.absoluteUrl(this.request.url); + const isSignedIn = OHIF.user.userLoggedIn() || OHIF.demoMode && OHIF.demoMode.userLoggedIn(); + const isDemoPage = OHIF.demoMode && this.url === "/demo-signin"; - if (OHIF.user.userLoggedIn()) { - next() - } else if (urlHasSignInResponse() === true) { - processSignInResponse().then(next); - } else { + if (isSignedIn || isDemoPage) + this.next(); + else if (urlHasSignInResponse() === true) + processSignInResponse().then(this.next); + else { + const redirect_uri = Meteor.absoluteUrl(OHIF.user.getOidcRedirectUri()); signIn({ redirect_uri }); } }); + +Router.route(OHIF.user.getOidcRedirectUri(), function() { + Router.go('/', {}, { replaceState: true }); +}, { name: 'oidc_redirect' }); + + diff --git a/Packages/ohif-viewerbase/assets/icons.svg b/Packages/ohif-viewerbase/assets/icons.svg index b15421f67..9b935e9bc 100644 --- a/Packages/ohif-viewerbase/assets/icons.svg +++ b/Packages/ohif-viewerbase/assets/icons.svg @@ -234,4 +234,41 @@ Password - + + Structured Report + + + + + + + + + + + Presentation State + + + + + + + + + + + + + + Google icon + + + + + + + + + + + \ No newline at end of file diff --git a/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js b/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js index 4f7b82dfb..bc40b549a 100644 --- a/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js +++ b/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js @@ -9,7 +9,7 @@ export function getWADORSImageUrl(instance, frame) { frame = (frame || 0) + 1; // Replaces /frame/1 by /frame/{frame} - wadorsuri = wadorsuri.replace(/(%2Fframes%2F)(\d+)/, `$1${frame}`); + wadorsuri = wadorsuri.replace(/(\/frames\/)(\d+)/, `$1${frame}`); return wadorsuri; } diff --git a/Packages/ohif-viewerbase/client/lib/helpers/formatPN.js b/Packages/ohif-viewerbase/client/lib/helpers/formatPN.js index e7ee7c7b9..b32378114 100644 --- a/Packages/ohif-viewerbase/client/lib/helpers/formatPN.js +++ b/Packages/ohif-viewerbase/client/lib/helpers/formatPN.js @@ -9,10 +9,28 @@ const formatPN = context => { // Convert the first ^ to a ', '. String.replace() only affects // the first appearance of the character. - const commaBetweenFirstAndLast = context.replace('^', ', '); + //const commaBetweenFirstAndLast = context.replace('^', ', '); // Replace any remaining '^' characters with spaces - const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + //const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // for cloud healthcare replace all ^ with ', ' + const cleaned = context.replace(/\^/g, ', '); + + // Trim any extraneous whitespace + return cleaned.trim(); +}; + +/** + * Formats a patient name for display purposes + */ +const reverseFormatPN = context => { + if (!context) { + context; + } + + // Replace any remaining '^' characters with spaces + const cleaned = context.replace(/, /g, '^'); // Trim any extraneous whitespace return cleaned.trim(); @@ -27,4 +45,8 @@ if (!Blaze._getGlobalHelper('formatPN')) { Blaze.registerHelper('formatPN', formatPN); } -export { formatPN }; +if (!Blaze._getGlobalHelper('reverseFormatPN')) { + Blaze.registerHelper('reverseFormatPN', reverseFormatPN); +} + +export { formatPN, reverseFormatPN }; diff --git a/Packages/ohif-viewerbase/client/lib/helpers/index.js b/Packages/ohif-viewerbase/client/lib/helpers/index.js index 867832cc5..f2ef53ea8 100644 --- a/Packages/ohif-viewerbase/client/lib/helpers/index.js +++ b/Packages/ohif-viewerbase/client/lib/helpers/index.js @@ -3,7 +3,7 @@ */ import { isTouchDevice } from './isTouchDevice'; -import { formatPN } from './formatPN'; +import { formatPN, reverseFormatPN } from './formatPN'; import { formatDA } from './formatDA'; import { formatTM } from './formatTM'; @@ -33,7 +33,8 @@ const helpers = { isTouchDevice, formatPN, formatDA, - formatTM + formatTM, + reverseFormatPN }; export { helpers }; diff --git a/Packages/ohif-viewerbase/client/lib/hotkeyUtils.js b/Packages/ohif-viewerbase/client/lib/hotkeyUtils.js index fdfd08157..e31c034a0 100644 --- a/Packages/ohif-viewerbase/client/lib/hotkeyUtils.js +++ b/Packages/ohif-viewerbase/client/lib/hotkeyUtils.js @@ -225,6 +225,16 @@ Meteor.startup(function() { name: 'Show/Hide Download Dialog', action: viewportUtils.toggleDownloadDialog, disabled: () => !viewportUtils.isDownloadEnabled() + }, + ps: { + name: 'Show/Hide Presentation State', + action: () => Session.set('MeasurementsReady', !Session.get('MeasurementsReady')), + disabled: () => false + }, + sr: { + name: 'Show/Hide Structured Report', + action: () => OHIF.ui.showDialog('structuredReportModal'), + disabled: () => false } }, true); diff --git a/Packages/ohif-viewerbase/client/lib/viewportUtils.js b/Packages/ohif-viewerbase/client/lib/viewportUtils.js index 86e719da3..f0507925e 100644 --- a/Packages/ohif-viewerbase/client/lib/viewportUtils.js +++ b/Packages/ohif-viewerbase/client/lib/viewportUtils.js @@ -167,6 +167,18 @@ const clearTools = () => { toolStateManager.clear(element); cornerstone.updateImage(element); }; +const hideTools = () => { + const element = getActiveViewportElement(); + const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; + toolStateManager.hide(element); + cornerstone.updateImage(element); +}; +const unhideTools = () => { + const element = getActiveViewportElement(); + const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; + toolStateManager.unhide(element); + cornerstone.updateImage(element); +}; const linkStackScroll = () => { const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; @@ -383,6 +395,8 @@ const viewportUtils = { flipH, resetViewport, clearTools, + hideTools, + unhideTools, linkStackScroll, toggleDialog, toggleCinePlay, diff --git a/clientImage/Dockerfile b/clientImage/Dockerfile new file mode 100644 index 000000000..9b925ad81 --- /dev/null +++ b/clientImage/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:stable-alpine-perl +COPY nginx.conf /etc/nginx/nginx.conf +COPY build /usr/share/nginx/html/ diff --git a/clientImage/howTo.md b/clientImage/howTo.md new file mode 100644 index 000000000..918286ced --- /dev/null +++ b/clientImage/howTo.md @@ -0,0 +1,17 @@ +## environment +[install meteor](https://www.meteor.com/install) +## download +```bash +git clone https://github.com/quantumsoftgroup/ViewersClone.git +cd ViewersClone +git checkout dev +``` +## build +```bash +cd OHIFViewer/ +METEOR_PACKAGE_DIRS="../Packages" meteor-build-client ../clientImage/build -s ../config/oidc.json +cd ../clientImage +docker build -t ohif-ghc . +docker run --env CLIENT_ID={$someID}.apps.googleusercontent.com --publish 3000:80 ohif-ghc + +``` diff --git a/clientImage/nginx.conf b/clientImage/nginx.conf new file mode 100644 index 000000000..db5518a7f --- /dev/null +++ b/clientImage/nginx.conf @@ -0,0 +1,44 @@ +worker_processes 1; +load_module modules/ngx_http_perl_module.so; +events { + worker_connections 1024; +} +env CLIENT_ID; +http { + include mime.types; + default_type application/octet-stream; + perl_set $client_id 'sub { return $ENV{"CLIENT_ID"}; }'; + + sendfile on; + keepalive_timeout 65; + + access_log off; + error_log off; + + server { + listen 80; + root /usr/share/nginx/html; + + location / { + try_files $uri @index; + } + + location @index { + add_header Cache-Control no-cache; + expires 0; + try_files /index.html =404; + } + + location ~ /(favicon.ico|favicon.png|robots.txt)$ { + expires 1y; + access_log off; + log_not_found off; + } + + location /gcloud-client-id { + return 200 '${client_id}'; + } + } +} + + diff --git a/config/oidc-demo.json b/config/oidc-demo.json new file mode 100644 index 000000000..710cdc771 --- /dev/null +++ b/config/oidc-demo.json @@ -0,0 +1,53 @@ +{ + "public": { + "clientOnly": true, + "googleCloud": true, + "ui": { + "studyListFunctionsEnabled": true, + "studyListDateFilterNumDays": false + }, + "servers": { + "dicomWeb": [{ + "name": "dcm4chee-oidc-Client", + "qidoSupportsIncludeField": false, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "metadataSource": "wado", + "requestOptions": { + "requestFromBrowser": true, + "logRequests": true, + "logResponses": false, + "logTiming": true + } + }, + { + "name": "demo-dcm4chee", + "wadoUriRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/wado", + "qidoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs", + "wadoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs", + "qidoSupportsIncludeField": false, + "imageRendering": "wadouri", + "thumbnailRendering": "wadors", + "requestOptions": { + "auth": "cloud:healthcare", + "requestFromBrowser": true, + "logRequests": true, + "logResponses": false, + "logTiming": true + } + }] + }, + "custom": { + "oidc": [{ + "authServerUrl": "https://accounts.google.com", + "authRedirectUri": "/_oauth/google", + "postLogoutRedirectUri": "/", + "clientId": "570420945968-pmtd0sjm7mmf3i5m7ld09aos1op3qva1.apps.googleusercontent.com", + "scope": "email profile openid https://www.googleapis.com/auth/cloud-platform.read-only https://www.googleapis.com/auth/cloud-healthcare", + "revokeUrl": "https://accounts.google.com/o/oauth2/revoke?token=" + }] + }, + "userAuthenticationRoutesEnabled": true, + "demoMode": true + } +} \ No newline at end of file diff --git a/config/oidc-dev.json b/config/oidc-dev.json new file mode 100644 index 000000000..710cdc771 --- /dev/null +++ b/config/oidc-dev.json @@ -0,0 +1,53 @@ +{ + "public": { + "clientOnly": true, + "googleCloud": true, + "ui": { + "studyListFunctionsEnabled": true, + "studyListDateFilterNumDays": false + }, + "servers": { + "dicomWeb": [{ + "name": "dcm4chee-oidc-Client", + "qidoSupportsIncludeField": false, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "metadataSource": "wado", + "requestOptions": { + "requestFromBrowser": true, + "logRequests": true, + "logResponses": false, + "logTiming": true + } + }, + { + "name": "demo-dcm4chee", + "wadoUriRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/wado", + "qidoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs", + "wadoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs", + "qidoSupportsIncludeField": false, + "imageRendering": "wadouri", + "thumbnailRendering": "wadors", + "requestOptions": { + "auth": "cloud:healthcare", + "requestFromBrowser": true, + "logRequests": true, + "logResponses": false, + "logTiming": true + } + }] + }, + "custom": { + "oidc": [{ + "authServerUrl": "https://accounts.google.com", + "authRedirectUri": "/_oauth/google", + "postLogoutRedirectUri": "/", + "clientId": "570420945968-pmtd0sjm7mmf3i5m7ld09aos1op3qva1.apps.googleusercontent.com", + "scope": "email profile openid https://www.googleapis.com/auth/cloud-platform.read-only https://www.googleapis.com/auth/cloud-healthcare", + "revokeUrl": "https://accounts.google.com/o/oauth2/revoke?token=" + }] + }, + "userAuthenticationRoutesEnabled": true, + "demoMode": true + } +} \ No newline at end of file diff --git a/config/oidc.json b/config/oidc.json new file mode 100644 index 000000000..53ad5b5da --- /dev/null +++ b/config/oidc.json @@ -0,0 +1,36 @@ +{ + "public": { + "clientOnly": true, + "googleCloud": true, + "ui": { + "studyListFunctionsEnabled": true, + "studyListDateFilterNumDays": false + }, + "servers": { + "dicomWeb": [{ + "name": "dcm4chee-oidc-Client", + "qidoSupportsIncludeField": false, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "metadataSource": "wado", + "requestOptions": { + "requestFromBrowser": true, + "logRequests": true, + "logResponses": false, + "logTiming": true + } + }] + }, + "custom": { + "oidc": [{ + "authServerUrl": "https://accounts.google.com", + "authRedirectUri": "/_oauth/google", + "postLogoutRedirectUri": "/", + "scope": "email profile openid https://www.googleapis.com/auth/cloud-platform.read-only https://www.googleapis.com/auth/cloud-healthcare", + "revokeUrl": "https://accounts.google.com/o/oauth2/revoke?token=" + }] + }, + "userAuthenticationRoutesEnabled": true, + "demoMode": false + } +} \ No newline at end of file diff --git a/config/orthancDIMSE.json b/config/orthancDIMSE.json index eeaafd64f..cd4acb29d 100644 --- a/config/orthancDIMSE.json +++ b/config/orthancDIMSE.json @@ -20,7 +20,7 @@ { "host": "0.0.0.0", "port": 11112, - "aeTitle": "OHIFDCM", + "aeTitle": "ORTHANC", "default": true, "server": true } diff --git a/ghc-dockerfile b/ghc-dockerfile new file mode 100644 index 000000000..a3d112119 --- /dev/null +++ b/ghc-dockerfile @@ -0,0 +1,36 @@ +# First stage of multi-stage build +# This stage is named 'builder' +# The data for this intermediary image is not included +# in the final image. +FROM node:8.10.0-slim as builder + +RUN apt-get update && apt-get install -y \ + curl \ + g++ \ + git \ + python \ + build-essential + +RUN curl https://install.meteor.com/ | sh +RUN npm install -g meteor-build-client-fixed2 +# Create a non-root user +RUN useradd -ms /bin/bash user +USER user +RUN mkdir /home/user/Viewers +COPY OHIFViewer/package.json /home/user/Viewers/OHIFViewer/ +ADD --chown=user:user . /home/user/Viewers + +WORKDIR /home/user/Viewers/OHIFViewer + +ENV METEOR_PACKAGE_DIRS=../Packages +ENV METEOR_PROFILE=1 +ARG OHIF_CONFIG_FILE=../config/oidc.json +RUN meteor npm install +RUN meteor-build-client-fixed2 /home/user/app/ -s $OHIF_CONFIG_FILE +RUN ls /home/user/app/ + +# Second stage of multi-stage build +FROM nginx:stable-alpine-perl +COPY ./clientImage/nginx.conf /etc/nginx/nginx.conf +COPY --from=builder /home/user/app /usr/share/nginx/html/ + diff --git a/package-lock.json b/package-lock.json index 5688b2b54..851cb319a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,445 +1,5 @@ { "name": "ohif-viewers-meta", "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "bcrypt": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-2.0.1.tgz", - "integrity": "sha512-DwB7WgJPdskbR+9Y3OTJtwRq09Lmm7Na6b+4ewvXjkD0nfNRi1OozxljHm5ETlDCBq9DTy04lQz+rj+T2ztIJg==", - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.9.1" - }, - "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.4", - "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.4.2", - "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.21", - "bundled": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "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.2.4", - "bundled": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - }, - "dependencies": { - "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.0", - "bundled": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.9.1", - "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.1.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.6", - "bundled": true, - "requires": { - "deep-extend": "~0.4.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.1", - "bundled": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.0.2", - "bundled": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - } - } - }, - "gitbook-plugin-edit-link": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gitbook-plugin-edit-link/-/gitbook-plugin-edit-link-2.0.2.tgz", - "integrity": "sha1-2PzZJ+ztgeemYqctWdtgnq/X5y8=" - }, - "gitbook-plugin-github": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gitbook-plugin-github/-/gitbook-plugin-github-3.0.0.tgz", - "integrity": "sha1-Z0V9+YpX6o75slGLiDQNs3ClMXs=" - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" - } - } + "lockfileVersion": 1 }