feat(google-cloud): Add support for Google Cloud Healthcare API DICOM Store Switch (#325)

* NOTASK: PoC for loading data from Healthcare API

* NOTASK: PoC for loading data from Healthcare API fix

* Add sample dialog for loading test dicom-picker

* Fix incorrect promise resolving

* NOTASK: PoC for loading dicom store

* Add web-components as git submodules. (Probably will be changed later)

* Add rough dicomStore picker

* Add dataset selector

* NOTASK: QIDO metadata first draft

* NOTASK: Not Implemented error for color images

* NOTASK: clean-up

* Dicom files uploader intermediate version

* NOTASK: Accept header

* NOTASK: docker configs

* NOTASK: fix

* NOTASK: howTo

* Add draft of DICOM uploader

* Add missing files

* Fix error with missing clientId

* Update submodule

* NOTASK: HowTo

* NOTASK: config for dev

* NOTASK: new docker file

* clean-up

* Update GCP web-components

* Add integration with async web components

* Fix errors in nginx.conf

* Structured reports views basic implementation

* NOTASK: qido -> wado

* NOTASK: config fix

* NOTASK: dirty copy-paste implementation

* Add "Change dicom store" button.
Add an ability to clear date.
Fix bug, when OAuth dosn't work if URL is not /

* Fix studylist filtering

* Fix dockerfile

* NOTASK: meteor update

* NOTASK: fix of package structure

* NOTASK: merge fix

* improvements

Move check for now SR into SR modal
Use simple button to open SR modal
Remove button styling
cleanup

* copy SR data retrieval to wado

* make search of SR simpler

* Codestyle fix

* NOTASK: palette color error message

* Fix date filter.
Fix server settings on first loading.
Add SR and PS buttons

* NOTASK: PS is now hidden by default

* NOTASK: dirty solution

* NOTASK:  new wadoimage lib version

* Fix PS and SR buttons

* Make modal dialogs vertically centered

* Update web components. Quick UI fixes

* Add missing files to the previous commit

* NOTASK:  show-hide for PS

* NOTASK:  show-hide for PS

* NOTASK:  fix of min-max PixelValue bug

* Add "sign out" functionality for Google OAuth. Update web-components.

* intermediate commit

* Add demo signin page

* Beautifying demo sign in page

* NOTASK:  merge

* NOTASK:  clean-up

* Use npm for getting healthcare-api-adapter instead of git submodule.

* NOTASK:  clean-up

* meteor update

* clean-up

* clean-up

* Clean up code

* Update healthcare-api-adapter version. Add location to dicom store path. Fix buttons style.

* Downgrade meteor and packages to master version. Use meteor-build-client-fixed2
This commit is contained in:
Egor Lezhnin 2019-03-22 22:42:26 +07:00 committed by Erik Ziegler
parent 1783351f57
commit 9486522552
68 changed files with 1422 additions and 514 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea/
.vscode/
.meteor/local
.meteor/meteorite
.meteor/dev_bundle

View File

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

View File

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

View File

@ -23,6 +23,8 @@
</div>
{{/section}}
{{/header}}
{{>Template.dynamic template=(choose this.template 'studylist') data=this}}
{{#if isStudyListReady}}
{{>Template.dynamic template=(choose this.template 'studylist') data=this}}
{{/if}}
</template>

View File

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

View File

@ -0,0 +1,11 @@
<template name="structuredReportModal">
{{#dialogForm (extend this
id='structuredReportModal'
dialogClass='themed modal-lg'
title='Structured report'
hideCancel=true
hideConfirm=true
)}}
<div id="root"></div>
{{/dialogForm}}
</template>

View File

@ -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('<div>No structured report found</div>');
}
function getMainDataHtml(data) {
const root = $('<div></div>');
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 = $('<div></div>');
const header = data.header;
const items = data.items || [];
if (header) {
root.append(`<h${level}>${header}</h${level}>`);
}
items.forEach(item => {
root.append(
item instanceof Object
? getContentSequenceHtml(item, level + 1)
: `<div>${item}</div>`
);
});
return root;
}
function getMainDataItemHtml(key, value) {
return $(`<div><b>${key}</b>: ${value}</div>`);
}

View File

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

View File

@ -0,0 +1,25 @@
<template name="demoSignin">
<div class="demoSignin">
<div class="demo-content">
<div class="demo-brand">
<svg class="logo-image">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ohif-logo"}}></use>
</svg>
<div class="logo-text">Open Health Imaging Foundation</div>
</div>
<div class="demoSigninButtons">
<button type="submit" id="google-login-button" class="button button--nostyle">
<span class="sign-in-with-google-icon-tile">
<svg class="google-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#google-icon"}}></use>
</svg></span>
<span>Sign in with Google</span>
</button>
<hr class="tint">
<button id="anonymous-login-button">
<span>Anonymous Sign in</span>
</button>
</div>
</div>
</div>
</template>

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import './demoSignin/demoSignin.html';
import './demoSignin/demoSignin.js';
import './demoSignin/demoSignin.styl';

View File

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

View File

@ -0,0 +1,2 @@
import './components';
import './routes.js';

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
healthcare-api-adapter/node_modules/
healthcare-api-adapter/public/
healthcare-api-adapter/src/

View File

@ -0,0 +1,12 @@
<template name="gcloudDialog">
<div id="{{this.id}}" class="modal fade gcloud-dialog {{this.class}}" tabindex="-1" role="dialog">
<div class="modal-dialog {{this.dialogClass}}" role="document">
<div class="modal-content">
<div class="modal-body">
{{>UI.contentBlock}}
</div>
<div class="modal-footer">{{>section 'dialogFooter'}}</div>
</div>
</div>
</div>
</template>

View File

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

View File

@ -0,0 +1,6 @@
@import '{ohif:design}/app'
.gcloud-dialog
display flex
.modal-dialog
margin auto

View File

@ -0,0 +1,9 @@
<template name="dicomStorePicker">
{{#gcloudDialog (extend this
id='dicomStorePicker' dialogClass='themed modal-lg'
)}}
<div class="dicom-store-picker">
<gcp-dataset-selector id={{datasetPickerId}} event={{eventName}} oidc-key={{oidcStorageKey}} can-close={{this.canClose}}></gcp-dataset-selector>
</div>
{{/gcloudDialog}}
</template>

View File

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

View File

@ -0,0 +1,5 @@
@import '{ohif:design}/app'
#dicomStorePicker
.modal-dialog
width 694px

View File

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

View File

@ -0,0 +1,9 @@
<template name="uploadStudiesDialog">
{{#gcloudDialog (extend this
id='dicomFilesUploader' dialogClass='themed modal-lg'
)}}
<div class="dicom-files-uploader">
<gcp-dicom-uploader id={{dicomFilesUploaderId}} event={{eventName}} oidc-key={{oidcStorageKey}} url={{url}}></gcp-dicom-uploader>
</div>
{{/gcloudDialog}}
</template>

View File

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

View File

@ -0,0 +1,8 @@
@import '{ohif:design}/app'
#dicomFilesUploader
.modal-dialog
.modal-body
padding 0
width 536px
max-height 563px

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,11 @@ const toolSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasu
label: 'Length',
optional: true,
decimal: true
},
dashed: {
type: String,
label: 'dashed',
optional: true
}
}]);

View File

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

View File

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

View File

@ -1 +1,2 @@
import './getCurrentServer.js';
import './applyCloudServerConfig.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
<template name="studylistResult">
<div class="studyListToolbar clearfix">
<div class="header pull-left">Study List</div>
<div class="header pull-left">
<span>Study List</span>
<span class="dicom-source">{{dicomSource}}</span>
</div>
<div class="studyCount pull-right">{{numberOfStudies}}</div>
<div class="pull-right">{{>studylistToolbar}}</div>
</div>
@ -56,7 +59,7 @@
class="form-control studylist-search"
id="modality">
</th>
<th class="studyDescription">
<!-- <th class="studyDescription">
<div id="_studyDescription" class="sortingCell">
<span>Study Description</span>
<i class="{{sortingColumnsIcons.studyDescription}}">&nbsp;</i>
@ -64,7 +67,7 @@
<input type="text"
class="form-control studylist-search"
id="studyDescription">
</th>
</th> -->
</tr>
</thead>
<tbody id="studyListData">

View File

@ -6,6 +6,7 @@ import { ReactiveDict } from 'meteor/reactive-dict';
import { moment } from 'meteor/momentjs:moment';
import { OHIF } from 'meteor/ohif:core';
Session.setDefault('showLoadingText', true);
Session.setDefault('serverError', false);
@ -39,6 +40,13 @@ Template.studylistResult.helpers({
return studies;
},
dicomSource() {
const config = Session.get('GCP_HEALTHCARE_CONFIG');;
if (!config)
return '';
return config.project + ' / ' + config.location + ' / ' + config.dataset + ' / ' + config.dicomStore
},
numberOfStudies() {
return OHIF.studylist.collections.Studies.find().count();
},
@ -75,8 +83,11 @@ let filter;
* @returns {*}
*/
function getFilter(filter) {
if (filter && filter.length && filter.substr(filter.length - 1) !== '*') {
filter += '*';
const server = OHIF.servers.getCurrentServer();
if (!server.isCloud) {
if (filter && filter.length && filter.substr(filter.length - 1) !== '*') {
filter += '*';
}
}
return filter;
@ -100,6 +111,9 @@ function replaceUndefinedColumnValue(text) {
function search(instance) {
OHIF.log.info('search()');
// Clear all current studies
OHIF.studylist.collections.Studies.remove({});
// Show loading message
Session.set('showLoadingText', true);
@ -111,10 +125,9 @@ function search(instance) {
const currentPage = instance.paginationData.currentPage.get();
// Create the filters to be used for the StudyList Search
const reverseFormatPN = Blaze._getGlobalHelper('reverseFormatPN');
filter = {
offset: rowsPerPage * currentPage,
limit: rowsPerPage,
patientName: getFilter($('input#patientName').val()),
patientName: reverseFormatPN(getFilter($('input#patientName').val())),
patientId: getFilter($('input#patientId').val()),
accessionNumber: getFilter($('input#accessionNumber').val()),
studyDescription: getFilter($('input#studyDescription').val()),
@ -122,7 +135,11 @@ function search(instance) {
studyDateTo,
modalitiesInStudy: $('input#modality').val() ? $('input#modality').val() : ''
};
const server = OHIF.servers.getCurrentServer();
if (!server.isCloud) {
filter.offset=rowsPerPage * currentPage;
filter.limit=rowsPerPage;
}
// Make sure that modality has a reasonable value, since it is occasionally
// returned as 'undefined'
const modality = replaceUndefinedColumnValue($('input#modality').val());
@ -143,10 +160,7 @@ function search(instance) {
// Loop through all identified studies
studies.forEach(study => {
// TODO: Why is this Modality filter different from QIDO?
if (!modality && !study.modalities.includes(modality)) {
return;
}
// Sometimes DICOM studies have incorrect Date entries with
// periods such as '1990.10.04'
@ -247,16 +261,30 @@ Template.studylistResult.onRendered(() => {
}
instance.datePicker = $studyDate.daterangepicker({
maxDate: today,
autoUpdateInput: true,
startDate: startDate,
endDate: endDate,
maxDate: false,
autoUpdateInput: false,
// startDate: startDate,
// endDate: endDate,
ranges: {
Empty: [],
Today: [today, today],
'Last 7 Days': [lastWeek, today],
'Last 30 Days': [lastMonth, today]
}
},
locale: { cancelLabel: 'Clear' }
}).data('daterangepicker');
instance.datePicker.autoUpdateInput = true;
$studyDate.on('apply.daterangepicker', function(ev, picker) {
if (picker.chosenLabel === 'Empty')
$(this).val('');
else
$(this).val(picker.startDate.format('MM/DD/YYYY') + ' - ' + picker.endDate.format('MM/DD/YYYY'));
$studyDate.trigger('change');
});
$studyDate.on('cancel.daterangepicker', function(ev, picker) {
$(this).val('');
$studyDate.trigger('change');
});
search(instance);
@ -307,9 +335,15 @@ Template.studylistResult.events({
dateRange = dateRange.replace(/ /g, '');
// Split dateRange into subdates
const dates = dateRange.split('-');
studyDateFrom = dates[0];
studyDateTo = dates[1];
if (dateRange) {
// Split dateRange into subdates
const dates = dateRange.split('-');
studyDateFrom = dates[0];
studyDateTo = dates[1];
} else {
studyDateFrom = '';
studyDateTo = '';
}
if (dateRange !== '') {
search(instance);

View File

@ -25,6 +25,11 @@ $bodyCellHeight = 40px
color: $tableTextSecondaryColor
line-height: $studyListToolbarHeight
.dicom-source
font-size: 18px;
font-weight: 400;
margin: 0 30px;
.studylistToolbar
height: $studyListToolbarHeight
line-height: $studyListToolbarHeight

View File

@ -8,5 +8,11 @@
<input id="btnImport" class='js-import-files' type="file" webkitdirectory directory multiple>
</div>
{{/if}}
{{#if changeDicomStoreSupported}}
{{#button class='btn btn-primary changeDicomStoreBtn'}}Change dicom store{{/button}}
{{/if}}
{{#if uploadSupported}}
{{#button class='btn btn-primary uploadStudiesBtn'}}Upload studies{{/button}}
{{/if}}
</div>
</template>

View File

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

View File

@ -25,3 +25,6 @@
overflow: hidden
position: absolute
z-index: -1
.uploadStudiesBtn
margin 0 20px

View File

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

View File

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

View File

@ -234,4 +234,41 @@
<title>Password</title>
<path d="M5 12h8v-3c0-2.203-1.797-4-4-4s-4 1.797-4 4v3zM18 13.5v9c0 0.828-0.672 1.5-1.5 1.5h-15c-0.828 0-1.5-0.672-1.5-1.5v-9c0-0.828 0.672-1.5 1.5-1.5h0.5v-3c0-3.844 3.156-7 7-7s7 3.156 7 7v3h0.5c0.828 0 1.5 0.672 1.5 1.5z"></path>
</symbol>
</svg>
<symbol id="icon-sr" viewBox="0 0 21 21">
<title>Structured Report</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-24.000000, -24.000000)">
<g transform="translate(25.000000, 25.000000)">
<path d="M16,17 L16,18 C16,18.5522847 15.5522847,19 15,19 L1,19 C0.44771525,19 0,18.5522847 0,18 L0,1 C0,0.44771525 0.44771525,1.59872116e-14 1,1.59872116e-14 L12.5857864,1.59872116e-14 C12.8510029,1.59872116e-14 13.1053568,0.10535684 13.2928932,0.292893219 L15.7071068,2.70710678 C15.8946432,2.89464316 16,3.14899707 16,3.41421356 L16,5" stroke="#91B9CD" stroke-width="2" stroke-linecap="round"></path>
<path d="M8.421,8.642 C8.13499857,8.642 7.90950083,8.70983265 7.7445,8.8455 C7.57949918,8.98116734 7.497,9.16083221 7.497,9.3845 C7.497,9.60816778 7.59783233,9.78599934 7.7995,9.918 C8.00116768,10.0500007 8.46682968,10.2058324 9.1965,10.3855 C9.92617032,10.5651676 10.4926647,10.8346649 10.896,11.194 C11.2993353,11.5533351 11.501,12.0776632 11.501,12.767 C11.501,13.4563368 11.2425026,14.0154979 10.7255,14.4445 C10.2084974,14.8735021 9.52833755,15.088 8.685,15.088 C7.46766058,15.088 6.37133821,14.6370045 5.396,13.735 L6.419,12.481 C7.24767081,13.2070036 8.01399648,13.57 8.718,13.57 C9.03333491,13.57 9.28083243,13.5021673 9.4605,13.3665 C9.64016756,13.2308327 9.73,13.0475012 9.73,12.8165 C9.73,12.5854988 9.63466762,12.4021673 9.444,12.2665 C9.25333238,12.1308327 8.87566949,11.993334 8.311,11.854 C7.41632886,11.6413323 6.76183541,11.3645017 6.3475,11.0235 C5.93316459,10.6824983 5.726,10.1471703 5.726,9.4175 C5.726,8.68782968 5.98816404,8.12500198 6.5125,7.729 C7.03683596,7.33299802 7.69132941,7.135 8.476,7.135 C8.9893359,7.135 9.5026641,7.22299912 10.016,7.399 C10.5293359,7.57500088 10.9766648,7.82433172 11.358,8.147 L10.489,9.401 C9.82166333,8.89499747 9.13233689,8.642 8.421,8.642 Z M19.201,9.863 C19.201,11.0950062 18.7133382,11.8906649 17.738,12.25 L19.685,15 L17.573,15 L15.868,12.547 L14.68,12.547 L14.68,15 L12.964,15 L12.964,7.311 L15.879,7.311 C17.0743393,7.311 17.9268308,7.51266465 18.4365,7.916 C18.9461692,8.31933535 19.201,8.96832886 19.201,9.863 Z M15.934,11.062 C16.5206696,11.062 16.9203323,10.9666676 17.133,10.776 C17.3456677,10.5853324 17.452,10.2828354 17.452,9.8685 C17.452,9.45416459 17.3420011,9.17000077 17.122,9.016 C16.9019989,8.86199923 16.5170027,8.785 15.967,8.785 L14.68,8.785 L14.68,11.062 L15.934,11.062 Z" fill="#91B9CD"></path>
</g>
</g>
</g>
</symbol>
<symbol id="icon-ps" viewBox="0 0 22 21">
<title>Presentation State</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-22.000000, -64.000000)">
<g transform="translate(22.000000, 65.000000)">
<path d="M3,3.55271368e-14 L19,3.55271368e-14 C20.6568542,3.66943598e-14 22,1.34314575 22,3 L22,12.4553925 C22,14.1122467 20.6568542,15.4553925 19,15.4553925 L3,15.4553925 C1.34314575,15.4553925 2.02906125e-16,14.1122467 0,12.4553925 L0,3 C-2.02906125e-16,1.34314575 1.34314575,3.5831496e-14 3,3.55271368e-14 Z M7.4305,4.311 L4.7135,4.311 L4.7135,12 L6.4295,12 L6.4295,9.855 L7.4525,9.855 C8.58917235,9.855 9.43433057,9.6350022 9.988,9.195 C10.5416694,8.7549978 10.8185,8.06017141 10.8185,7.1105 C10.8185,6.16082858 10.5490027,5.45683562 10.01,4.9985 C9.47099731,4.54016437 8.61117257,4.311 7.4305,4.311 Z M14.6355,5.642 C15.3468369,5.642 16.0361633,5.89499747 16.7035,6.401 L17.5725,5.147 C17.1911648,4.82433172 16.7438359,4.57500088 16.2305,4.399 C15.7171641,4.22299912 15.2038359,4.135 14.6905,4.135 C13.9058294,4.135 13.251336,4.33299802 12.727,4.729 C12.202664,5.12500198 11.9405,5.68782969 11.9405,6.4175 C11.9405,7.14717032 12.1476646,7.6824983 12.562,8.0235 C12.9763354,8.36450171 13.6308289,8.64133227 14.5255,8.854 C15.0901695,8.99333403 15.4678324,9.13083266 15.6585,9.2665 C15.8491676,9.40216735 15.9445,9.58549885 15.9445,9.8165 C15.9445,10.0475012 15.8546676,10.2308327 15.675,10.3665 C15.4953324,10.5021673 15.2478349,10.57 14.9325,10.57 C14.2284965,10.57 13.4621708,10.2070036 12.6335,9.481 L11.6105,10.735 C12.5858382,11.6370045 13.6821606,12.088 14.8995,12.088 C15.7428376,12.088 16.4229974,11.8735021 16.94,11.4445 C17.4570026,11.0154979 17.7155,10.4563368 17.7155,9.767 C17.7155,9.07766322 17.5138354,8.55333513 17.1105,8.194 C16.7071647,7.83466487 16.1406703,7.56516757 15.411,7.3855 C14.6813297,7.20583244 14.2156677,7.05000066 14.014,6.918 C13.8123323,6.78599934 13.7115,6.60816779 13.7115,6.3845 C13.7115,6.16083222 13.7939992,5.98116735 13.959,5.8455 C14.1240008,5.70983266 14.3494986,5.642 14.6355,5.642 Z M7.5845,8.37 L6.4295,8.37 L6.4295,5.785 L7.4085,5.785 C7.97316949,5.785 8.389332,5.87849906 8.657,6.0655 C8.92466801,6.25250093 9.0585,6.56783111 9.0585,7.0115 C9.0585,7.45516888 8.95583436,7.79249884 8.7505,8.0235 C8.54516564,8.25450115 8.15650286,8.37 7.5845,8.37 Z" fill="#91B9CD"></path>
<path d="M11,15 L11,18" stroke="#91B9CD" stroke-width="2" stroke-linecap="round"></path>
<path d="M11,-1.11022302e-15 L11,1" stroke="#91B9CD" stroke-width="2" stroke-linecap="round"></path>
<path d="M7,19 L11,18" stroke="#91B9CD" stroke-width="2" stroke-linecap="round"></path>
<path d="M11,19 L15,18" stroke="#91B9CD" stroke-width="2" stroke-linecap="round" transform="translate(13.000000, 18.000000) scale(-1, 1) translate(-13.000000, -18.000000) "></path>
</g>
</g>
</g>
</symbol>
<symbol id="google-icon" viewBox="0 0 24 24">
<desc>Google icon</desc>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(3.000000, 3.000000)">
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4"></path>
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853"></path>
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05"></path>
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335"></path>
<polygon id="Shape" points="0 0 18 0 18 18 0 18"></polygon>
</g>
</g>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

3
clientImage/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM nginx:stable-alpine-perl
COPY nginx.conf /etc/nginx/nginx.conf
COPY build /usr/share/nginx/html/

17
clientImage/howTo.md Normal file
View File

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

44
clientImage/nginx.conf Normal file
View File

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

53
config/oidc-demo.json Normal file
View File

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

53
config/oidc-dev.json Normal file
View File

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

36
config/oidc.json Normal file
View File

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

View File

@ -20,7 +20,7 @@
{
"host": "0.0.0.0",
"port": 11112,
"aeTitle": "OHIFDCM",
"aeTitle": "ORTHANC",
"default": true,
"server": true
}

36
ghc-dockerfile Normal file
View File

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

442
package-lock.json generated
View File

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