LT-67; OHIF-78: Merging server configuration and CINE dialog changes from downstream (Ascend)

This commit is contained in:
Bruno Alves de Faria 2016-09-29 12:25:04 -03:00
parent da7202de0f
commit 8d5d1ee360
47 changed files with 516 additions and 333 deletions

View File

@ -1,3 +1,4 @@
accounts-base@1.2.11
aldeed:simple-schema@1.5.3
aldeed:template-extension@4.0.0
allow-deny@1.0.5
@ -24,6 +25,7 @@ cornerstone@0.0.1
ddp@1.2.5
ddp-client@1.3.1
ddp-common@1.2.6
ddp-rate-limiter@1.0.5
ddp-server@1.3.10
deps@1.0.12
design@0.0.1
@ -51,6 +53,7 @@ johdirr:meteor-git-rev@0.0.4
jquery@1.11.9
launch-screen@1.0.12
livedata@1.0.18
localstorage@1.0.11
logging@1.1.15
mdg:validation-error@0.5.1
meteor@1.2.17
@ -78,12 +81,14 @@ practicalmeteor:chai@2.1.0_1
practicalmeteor:loglevel@1.2.0_2
promise@0.8.7
random@1.0.10
rate-limit@1.0.5
reactive-dict@1.1.8
reactive-var@1.0.10
reload@1.1.10
retry@1.0.8
routepolicy@1.0.11
rwatts:uuid@0.1.0
service-configuration@1.0.10
session@1.1.6
shell-server@0.2.1
silentcicero:jszip@0.0.4

View File

@ -11,19 +11,36 @@
Open Health Imaging Foundation
</div>
</a>
{{ #if studyListToggleText }}
<a class="btn studyListLinkSection pull-left m-x-1 p-x-2 p-y-0 js-toggle-studyList">
{{studyListToggleText}}
</a>
{{ /if }}
<a class="btn pull-right p-y-0"
data-toggle="modal"
data-target="#aboutModal">
About
</a>
{{#if studyListToggleText}}
<a class="btn studyListLinkSection pull-left m-x-1 p-x-2 p-y-0 js-toggle-studyList">
{{studyListToggleText}}
</a>
{{/if}}
<span class="pull-right">
<div class="dropdown">
<div class="dropdown-toggle noselect clearfix" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="pull-left m-r-1">Options</span>
<div class="menuButton pull-right"><span class="caret-down"></span></div>
</div>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a data-toggle="modal" data-target="#serverInformationModal">
<i class="fa fa-server fa-lg"></i> Server Information
</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a data-toggle="modal" data-target="#aboutModal">
<i class="fa fa-info"></i> About
</a>
</li>
</ul>
</div>
</span>
</div>
</div>
{{> worklist }}
{{> aboutModal }}
{{> viewSeriesDetailsModal }}
</template>
{{>serverInformationModal}}
{{>worklist}}
{{>aboutModal}}
{{>viewSeriesDetailsModal}}
</template>

View File

@ -1,3 +1,6 @@
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
const worklistContentId = 'worklistTab';
let lastContentId;
@ -39,7 +42,7 @@ Template.ohifViewer.helpers({
return 'Study list';
}
},
onStudyList() {
return (Session.get('activeContentId') === 'worklistTab');
}

View File

@ -221,6 +221,10 @@ Template.toolbarSection.onRendered(function() {
instance.$('#layout').dropdown();
if (OHIF.uiSettings.showCineDialogOnRendered) {
toggleCineDialog();
}
// Set disabled/enabled tool buttons that are set in toolManager
const states = toolManager.getToolDefaultStates();
const disabledToolButtons = states.disabledToolButtons;

View File

@ -31,6 +31,9 @@ label.form-group
margin-left: auto
margin-right: auto
.height-auto
height: auto !important
.caret-down
display: inline-block
width: 0

View File

@ -1,9 +1,9 @@
import { _ } from 'meteor/underscore';
// Uses NodeJS 'net'
// https://nodejs.org/api/net.html
var net = Npm.require('net'),
Socket = net.Socket;
var DEFAULT_MAX_PACKAGE_SIZE = 32768;
var net = Npm.require('net');
var Socket = net.Socket;
Connection = function(options) {
EventEmitter.call(this);
@ -16,10 +16,7 @@ Connection = function(options) {
}
}, options);
this.peers = {};
this.peerSockets = {};
this.defaultPeer = null;
this.defaultServer = null;
this.reset();
};
util.inherits(Connection, EventEmitter);
@ -30,15 +27,29 @@ var StoreHandle = function() {
util.inherits(StoreHandle, EventEmitter);
Connection.prototype.reset = function() {
this.defaultPeer = null;
this.defaultServer = null;
_.each(this.peers, peerInfo => {
_.each(peerInfo.sockets, socket => socket.emit('close'));
});
this.peers = {};
};
Connection.prototype.addPeer = function(options) {
if (!options.aeTitle || !options.host || !options.port) {
return false;
}
this.peers[options.aeTitle] = {
var peer = {
host: options.host,
port: options.port
port: options.port,
sockets: {}
};
this.peers[options.aeTitle] = peer;
if (options.default) {
if (options.server) {
this.defaultServer = options.aeTitle;
@ -49,22 +60,21 @@ Connection.prototype.addPeer = function(options) {
if (options.server) {
//start listening
var server = net.createServer();
server.listen(options.port, options.host, function() {
peer.server = net.createServer();
peer.server.listen(options.port, options.host, function() {
console.log('listening on %j', this.address());
});
server.on('error', function(err) {
peer.server.on('error', function(err) {
console.log('server error %j', err);
});
var o = this;
server.on('connection', function(nativeSocket) {
peer.server.on('connection', nativeSocket => {
//incoming connections
var socket = new CSocket(nativeSocket, o.options);
o.addSocket(options.aeTitle, socket);
var socket = new CSocket(nativeSocket, this.options);
this.addSocket(options.aeTitle, socket);
//close server on close socket
socket.on('close', function() {
server.close();
peer.server.close();
});
});
}
@ -81,8 +91,8 @@ Connection.prototype.selectPeer = function(aeTitle) {
Connection.prototype._sendFile = function(socket, sHandle, file, maxSend, metaLength, list) {
var fileNameText = typeof file.file === 'string' ? file.file : 'buffer';
console.log('Sending file ' + fileNameText);
var useContext = socket.getContextByUID(file.context),
self = this;
var useContext = socket.getContextByUID(file.context);
var self = this;
PDU.generatePDatas(useContext.id, file.file, maxSend, null, metaLength, function(err, handle) {
if (err) {
@ -122,13 +132,14 @@ Connection.prototype._sendFile = function(socket, sHandle, file, maxSend, metaLe
};
Connection.prototype.storeInstances = function(fileList) {
var contexts = {},
read = 0,
length = fileList.length,
toSend = [],
self = this,
handle = new StoreHandle();
var contexts = {};
var read = 0;
var length = fileList.length;
var toSend = [];
var self = this;
var handle = new StoreHandle();
var lastProcessedMetaLength;
fileList.forEach(function(bufferOrFile) {
var fileNameText = typeof bufferOrFile === 'string' ? bufferOrFile : 'buffer';
DicomMessage.readMetaHeader(bufferOrFile, function(err, metaMessage, metaLength) {
@ -144,9 +155,9 @@ Connection.prototype.storeInstances = function(fileList) {
console.log('Dicom file ' + (typeof bufferOrFile === 'string' ? bufferOrFile : 'buffer') + ' found');
lastProcessedMetaLength = metaLength;
var syntax = metaMessage.getValue(0x00020010),
sopClassUID = metaMessage.getValue(0x00020002),
instanceUID = metaMessage.getValue(0x00020003);
var syntax = metaMessage.getValue(0x00020010);
var sopClassUID = metaMessage.getValue(0x00020002);
var instanceUID = metaMessage.getValue(0x00020003);
if (!contexts[sopClassUID]) {
contexts[sopClassUID] = [];
@ -173,23 +184,22 @@ Connection.prototype.storeInstances = function(fileList) {
// Starts to send dcm files
sendProcessedFiles = function(self, contexts, toSend, handle, metaLength) {
var useContexts = [];
for (var context in contexts) {
var useSyntaxes = contexts[context];
_.each(contexts, (useSyntaxes, context) => {
if (useSyntaxes.length > 0) {
useContexts.push({
context: context,
syntaxes: contexts[context]
syntaxes: useSyntaxes
});
} else {
throw 'No syntax specified for context ' + context;
}
}
});
self.associate({
contexts: useContexts
}, function(ac) {
var maxSend = ac.getMaxSize(),
next = toSend.shift();
var maxSend = ac.getMaxSize();
var next = toSend.shift();
self._sendFile(this, handle, next, maxSend, metaLength, toSend);
});
@ -202,8 +212,8 @@ Connection.prototype.storeResponse = function(messageId, msg) {
var status = rq.listener[2].call(this, msg);
if (status !== undefined && status !== null && rq.command.store) {
//store ok, ready to send c-store-rsp
var storeSr = rq.command.store,
replyMessage = storeSr.replyWith(status);
var storeSr = rq.command.store;
var replyMessage = storeSr.replyWith(status);
replyMessage.setAffectedSOPInstanceUID(this.lastCommand.getSOPInstanceUID());
replyMessage.setReplyMessageId(this.lastCommand.messageId);
this.sendMessage(replyMessage, null, null, storeSr);
@ -215,8 +225,8 @@ Connection.prototype.storeResponse = function(messageId, msg) {
Connection.prototype.allClosed = function() {
var allClosed = true;
for (var i in o.peerSockets) {
if (Object.keys(o.peerSockets[ae]).length > 0) {
for (var i in this.peers) {
if (Object.keys(peers[i].sockets).length > 0) {
allClosed = false;
break;
}
@ -225,34 +235,30 @@ Connection.prototype.allClosed = function() {
return allClosed;
};
Connection.prototype.addSocket = function(ae, socket) {
if (!this.peerSockets[ae]) {
this.peerSockets[ae] = {};
}
Connection.prototype.addSocket = function(hostAE, socket) {
var peerInfo = this.selectPeer(hostAE);
this.peerSockets[ae][socket.id] = socket;
peerInfo.sockets[socket.id] = socket;
var o = this;
socket.on('close', function() {
if (o.peerSockets[ae][this.id]) {
delete o.peerSockets[ae][this.id];
if (peerInfo.sockets[this.id]) {
delete peerInfo.sockets[this.id];
}
});
};
Connection.prototype.associate = function(options, callback) {
var hostAE = options.hostAE ? options.hostAE : this.defaultPeer,
sourceAE = options.sourceAE ? options.sourceAE : this.defaultServer;
var hostAE = options.hostAE ? options.hostAE : this.defaultPeer;
var sourceAE = options.sourceAE ? options.sourceAE : this.defaultServer;
if (!hostAE || !sourceAE) {
throw 'Peers not provided or no defaults in settings';
}
var peerInfo = this.selectPeer(hostAE),
nativeSocket = new Socket();
var peerInfo = this.selectPeer(hostAE);
var nativeSocket = new Socket();
var socket = new CSocket(nativeSocket, this.options),
o = this;
var socket = new CSocket(nativeSocket, this.options);
if (callback) {
socket.once('associated', callback);
@ -268,9 +274,9 @@ Connection.prototype.associate = function(options, callback) {
nativeSocket.connect({
host: peerInfo.host,
port: peerInfo.port
}, function() {
}, () => {
//connected
o.addSocket(hostAE, socket);
this.addSocket(hostAE, socket);
if (options.contexts) {
socket.setPresentationContexts(options.contexts);
@ -282,4 +288,4 @@ Connection.prototype.associate = function(options, callback) {
});
return socket;
};
};

View File

@ -1,12 +1,14 @@
var Future = Npm.require('fibers/future');
DIMSE = {};
DIMSE = {
connection: new Connection({
vr: {
split: false
}
})
};
var conn = new Connection({
vr: {
split: false
}
});
var conn = DIMSE.connection;
var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
return {
@ -64,35 +66,6 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
};
};
Meteor.startup(function() {
if (Meteor.settings.defaultServiceType !== 'dimse') {
return;
}
if (!Meteor.settings.servers.dimse ||
!Meteor.settings.servers.dimse.length) {
console.error('dimse-config: ' + 'No DIMSE Servers provided.');
throw new Meteor.Error('dimse-config', 'No DIMSE Servers provided.');
}
// TODO: [custom-servers] use active server and check if type is DIMSE
var peers = Meteor.settings.servers.dimse[0].peers;
if (!peers || !peers.length) {
console.error('dimse-config: ' + 'No DIMSE Peers provided.');
throw new Meteor.Error('dimse-config', 'No DIMSE Peers provided.');
}
console.log('Adding DIMSE peers');
try {
peers.forEach(function(peer) {
conn.addPeer(peer);
});
} catch(error) {
console.error('dimse-addPeers: ' + error);
throw new Meteor.Error('dimse-addPeers', error);
}
});
DIMSE.associate = function(contexts, callback, options) {
var defaults = {
contexts: contexts
@ -239,10 +212,10 @@ DIMSE.retrieveInstancesByStudyOnly = function(studyInstanceUID, params, options)
0x0020000E: '',
0x00200011: ''
};
var result = this.findSeries(Object.assign(defaultParams, params)),
series = [],
conn = this,
allInstances = [];
var result = this.findSeries(Object.assign(defaultParams, params));
var series = [];
var conn = this;
var allInstances = [];
result.on('result', function(msg) {
series.push(msg);

View File

@ -1,8 +1,5 @@
import { Mongo } from 'meteor/mongo';
// Servers describe the DICOM servers configurations
Servers = new Mongo.Collection('servers');
Timepoints = new Mongo.Collection('timepoints');
Studies = new Mongo.Collection('studies');
Measurements = new Mongo.Collection('measurements');

View File

@ -1,2 +1 @@
Meteor.subscribe('reviewers');
Meteor.subscribe('servers');

View File

@ -1,30 +0,0 @@
Template.serverInformationList.onCreated(() => {
const instance = Template.instance();
instance.api = {
add: () => instance.data.mode.set('create'),
edit(server) {
instance.data.currentItem.set(server);
instance.data.mode.set('edit');
},
delete(server) {
Meteor.call('serverRemove', server._id, error => {
// TODO: check for errors: not-authorized, data-write
});
},
use(server) {
Meteor.call('serverSetActive', server._id, error => {
// TODO: [custom-servers] check for errors: not-authorized, data-write
});
}
};
});
Template.serverInformationList.helpers({
isActive: function(server) {
return server._id === Meteor.user().profile.activeServer;
},
servers: function() {
return Servers.find().fetch();
}
});

View File

@ -203,7 +203,6 @@ Package.onUse(function(api) {
// Server functions
api.addFiles('server/publications.js', 'server');
api.addFiles('server/servers.js', 'server');
api.addFiles('server/methods.js', [ 'server' ]);
api.addFiles('server/reviewers.js', [ 'server' ]);
api.addFiles('server/createDemoUser.js', [ 'server' ]);
@ -242,23 +241,6 @@ Package.onUse(function(api) {
api.addFiles('lib/handleMeasurementModified.js', 'client');
api.addFiles('lib/handleMeasurementRemoved.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationDicomWeb/serverInformationDicomWeb.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationDicomWeb/serverInformationDicomWeb.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationDimse/serverInformationDimse.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationDimse/serverInformationDimse.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationForm.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationForm.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationFormField.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationList/serverInformationList.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationList/serverInformationList.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.styl', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
// API classes
api.addFiles('client/api/timepoint.js');
api.addFiles('client/api/measurement.js');
@ -299,7 +281,6 @@ Package.onUse(function(api) {
api.export('TrialCriteriaTypes', 'client');
// Export collections spanning both client and server
api.export('Servers', [ 'client', 'server' ]);
api.export('AdditionalFindings', [ 'client', 'server' ]);
api.export('ImageMeasurements', [ 'client', 'server' ]);
api.export('Measurements', [ 'client', 'server' ]);

View File

@ -46,10 +46,6 @@ Meteor.publish('reviewers', function() {
return Reviewers.find();
});
Meteor.publish('servers', () => {
return Servers.find();
});
// Temporary fix to drop all Collections on server restart
// http://stackoverflow.com/questions/23891631/meteor-how-can-i-drop-all-mongo-collections-and-clear-all-data-on-startup
Meteor.startup(function() {

View File

@ -126,7 +126,7 @@ OHIF.mixins.schemaData = new OHIF.Mixin({
// Stop here if there's no schema data for current key
if (!currentSchema) {
return;
return value;
}
// Check if the schema is a Number

View File

@ -1,7 +1,6 @@
<template name="cineDialog">
{{#form id='cineDialog' class='noselect' schema=instance.schema api=instance.api}}
{{>inputHidden key='intervalId'}}
<h5>CINE Controls</h5>
<div class="cine-navigation">
<div class="btn-group">
{{#button disabled=(displaySetDisabled false) action='displaySetPrevious' title='Previous display set'}}
@ -20,9 +19,6 @@
{{#button action='cinePrevious' title='Previous image'}}
<i class="fa fa-step-backward"></i>
{{/button}}
{{#button action='cineSlowDown' title='Slow playback'}}
<i class="fa fa-backward"></i>
{{/button}}
{{#button action='cineToggle' title='Play / Stop' class=(valueIf isPlaying 'active' '')}}
{{#if isPlaying}}
<i class="fa fa-fw fa-stop"></i>
@ -30,9 +26,6 @@
<i class="fa fa-fw fa-play"></i>
{{/if}}
{{/button}}
{{#button action='cineSpeedUp' title='Increase playback speed'}}
<i class="fa fa-forward"></i>
{{/button}}
{{#button action='cineNext' title='Next image'}}
<i class="fa fa-step-forward"></i>
{{/button}}
@ -42,14 +35,11 @@
</div>
</div>
<div class="cine-options">
{{>inputCheckbox key='loop' labelClass='pull-right m-a-0'}}
<div class="fps-section">
{{#inputRange key='framesPerSecond' class='p-a-0' labelClass='form-group m-a-0' labelAsDiv=true}}
{{#section 'labelAfterText'}}:
<span id="fps">{{framerate}}</span>
{{/section}}
{{/inputRange}}
</div>
<span id="fps">{{framerate}} fps</span>
</div>
{{/form}}
</template>

View File

@ -21,7 +21,7 @@ Template.cineDialog.onCreated(() => {
},
framesPerSecond: {
type: Number,
label: 'Cine Speed',
label: '',
defaultValue: 24,
min: 1,
max: 90,

View File

@ -1,19 +1,19 @@
@import "{design}/app.styl"
#cineDialog
theme('border', '1px solid $uiBorderColor', 0.95)
theme('border', '2px solid $uiBorderColor', 0.95)
theme('background', '$uiGrayDarkest', 0.95)
theme('color', '$textSecondaryColor')
border-radius: 8px
bottom: 3px
box-sizing: border-box
height: 150px
height: 80px
left: 50%
overflow: hidden
padding: 10px
position: absolute
right: 3px
top: 50%
top: 2%
width: 300px
z-index: 1000
transform(translateX(-50%) translateY(-50%))
@ -36,9 +36,10 @@
&:active, &.active
theme('color', '$activeColor')
.cine-navigation
position: absolute
right: 24px
right: 4px
top: 4px
.btn
@ -49,12 +50,19 @@
line-height: 36px
.cine-controls
display: table
margin: 0 auto
left: 0px
.cine-options
padding: 5px 0
overflow: auto
padding: 0px 0
.fps-section
width: 175px
float: left
#fps
float: right
padding: 5px 10px 0 0
.loop-section
padding-right: 12px

View File

@ -1,3 +1,9 @@
import { Mongo } from 'meteor/mongo';
StudyImportStatus = new Mongo.Collection('studyImportStatus');
StudyImportStatus = new Mongo.Collection('studyImportStatus');
// Servers describe the DICOM servers configurations
Servers = new Mongo.Collection('servers');
// CurrentServer is a single document collection to describe which of the Servers is being used
CurrentServer = new Mongo.Collection('currentServer');

View File

@ -0,0 +1,14 @@
/**
* Retrieves the current server configuration used to retrieve studies
*/
getCurrentServer = () => {
const currentServer = CurrentServer.findOne();
if (!currentServer) {
return;
}
const serverConfiguration = Servers.findOne({ _id: currentServer.serverId });
return serverConfiguration;
};

View File

@ -14,6 +14,12 @@ const serverTypeDefinitions = {
optional: true
};
const wadoUriRootDefinitions = {
type: String,
label: 'WADO URI root',
max: 1000
};
export const DICOMWebRequestOptions = new SimpleSchema({
auth: {
type: String,
@ -40,11 +46,7 @@ export const DICOMWebRequestOptions = new SimpleSchema({
export const DICOMWebServer = new SimpleSchema({
name: serverNameDefinitions,
type: serverTypeDefinitions,
wadoUriRoot: {
type: String,
label: 'WADO URI root',
max: 1000
},
wadoUriRoot: wadoUriRootDefinitions,
wadoRoot: {
type: String,
label: 'WADO root',
@ -106,7 +108,7 @@ export const DIMSEPeer = new SimpleSchema({
},
supportsInstanceRetrievalByStudyUid: {
type: Boolean,
label: 'Supports instance retrieval by StudyUid?',
label: 'Supports instance retrieval by StudyUid',
defaultValue: true
}
});
@ -114,6 +116,7 @@ export const DIMSEPeer = new SimpleSchema({
export const DIMSEServer = new SimpleSchema({
name: serverNameDefinitions,
type: serverTypeDefinitions,
wadoUriRoot: wadoUriRootDefinitions,
peers: {
type: [DIMSEPeer],
label: 'Peer List',
@ -141,6 +144,11 @@ export const UISettings = new SimpleSchema({
type: Boolean,
label: 'The UP/DOWN display set navigation buttons will iterate over all the viewports at once?',
defaultValue: false
},
showCineDialogOnRendered: {
type: Boolean,
label: 'The Cine dialog will be displayed by default',
defaultValue: false
}
});

View File

@ -0,0 +1,2 @@
Meteor.subscribe('servers');
Meteor.subscribe('currentServer');

View File

@ -1,8 +1,15 @@
<template name="serverInformationDimse">
<div class="row">
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
</div>
</div>
<div class="panel panel-default panel-body">
<div class="clearfix">
<h4 class="pull-left">Peer List</h4>
<button class="btn btn-primary pull-right js-new-peer">New peer</button>
{{#button class='btn btn-primary pull-right'
action='newPeer' params=peer
}}New peer{{/button}}
</div>
<hr>
{{#group class='row' key='peers' arrayValues=true}}
@ -25,16 +32,24 @@
<div class="col-lg-12">
<strong>Settings</strong>
<div class="form-group">
<div class="form-control clearfix">
<div class="form-control height-auto clearfix">
<div class="checkbox pull-left">
{{>inputCheckbox key='default'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='server'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='supportsInstanceRetrievalByStudyUid'}}
</div>
</div>
</div>
</div>
<div class="col-lg-12 text-right">
{{#button class='btn btn-sm btn-danger'
action='removePeer' params=@index
}}Remove{{/button}}
</div>
</div>
</div>
{{/group}}

View File

@ -7,6 +7,23 @@ Template.serverInformationDimse.onCreated(() => {
instance.peers.set(currentItem.peers || []);
}
});
const api = instance.data.api;
_.extend(api, {
newPeer() {
const peers = instance.peers.get();
peers.push({});
instance.peers.set(peers);
},
removePeer(peerIndex) {
const peers = instance.peers.get();
peers.splice(peerIndex, 1);
instance.peers.set(peers);
const data = instance.data.currentItem.get();
data.peers = peers;
Tracker.afterFlush(() => instance.data.form.value(data));
}
});
});
Template.serverInformationDimse.onRendered(() => {
@ -19,12 +36,3 @@ Template.serverInformationDimse.onRendered(() => {
}
});
});
Template.serverInformationDimse.events({
'click .js-new-peer'(event, instance) {
event.preventDefault();
const peers = instance.peers.get();
peers.push({});
instance.peers.set(peers);
}
});

View File

@ -1,5 +1,5 @@
<template name="serverInformationForm">
{{#form class='server-information-form' schema=instance.currentSchema api=instance.api}}
{{#form class='server-information-form' schema=instance.currentSchema api=this.api}}
{{>inputHidden key='_id'}}
<div class="row">
<div class="col-lg-6">

View File

@ -1,13 +1,13 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { DICOMWebServer as dicomSchema } from 'meteor/worklist/both/schema';
import { DIMSEServer as dimseSchema } from 'meteor/worklist/both/schema';
import { DICOMWebServer as dicomSchema } from 'meteor/worklist/both/schema.js';
import { DIMSEServer as dimseSchema } from 'meteor/worklist/both/schema.js';
Template.serverInformationForm.onCreated(() => {
const instance = Template.instance();
instance.api = {
instance.data.api = {
save() {
// Stop here if the form validation fails
if (!instance.data.form.validate()) {

View File

@ -0,0 +1,47 @@
Template.serverInformationList.onCreated(() => {
const instance = Template.instance();
instance.api = {
add: () => instance.data.mode.set('create'),
edit(server) {
instance.data.currentItem.set(server);
instance.data.mode.set('edit');
},
delete(server) {
// TODO: Replace this for confirmation dialog after LT-refactor is merged back to master
if (!confirm('Are you sure you want to remove this peer?')) {
return;
}
Meteor.call('serverRemove', server._id, error => {
// TODO: check for errors: data-write
});
},
use(server) {
Meteor.call('serverSetActive', server._id, error => {
// TODO: check for errors: data-write
});
}
};
});
Template.serverInformationList.helpers({
isActive: function(server) {
const currentServer = CurrentServer.findOne();
if (!currentServer) {
return;
}
return server._id === currentServer.serverId;
},
servers: function() {
const options = {
sort: {
origin: -1,
type: 1,
name: 1
}
};
return Servers.find({}, options).fetch();
}
});

View File

@ -34,6 +34,11 @@ switchToTab = function(contentId) {
var container;
if (contentId === 'worklistTab') {
container = $('.tab-content').find('#worklistTab').get(0);
if (!container) {
return;
}
var worklistContainer = document.createElement('div');
worklistContainer.classList.add('worklistContainer');
container.appendChild(worklistContainer);

View File

@ -1,3 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
Worklist = {
functions: {},
callbacks: {}
@ -10,4 +13,19 @@ function dblClickOnStudy(data) {
// Use the formatPN template helper to clean up the patient name
var title = formatPN(data.patientName);
openNewTab(data.studyInstanceUid, title);
}
}
let currentServerChangeHandlerFirstRun = true;
const currentServerChangeHandler = () => {
if (currentServerChangeHandlerFirstRun) {
currentServerChangeHandlerFirstRun = false;
return;
}
switchToTab('worklistTab');
};
CurrentServer.find().observe({
added: currentServerChangeHandler,
changed: currentServerChangeHandler
});

View File

@ -16,11 +16,12 @@ Package.onUse(function (api) {
api.use('rwatts:uuid');
api.use('silentcicero:jszip');
api.use('aldeed:simple-schema');
api.use('accounts-base');
// Note: MomentJS appears to be required for Bootstrap3 Datepicker, but not a dependency for some reason
api.use('momentjs:moment');
api.use('gilbertwat:bootstrap3-daterangepicker');
api.use('gilbertwat:bootstrap3-daterangepicker');
// Our custom packages
api.use('ohif:core');
@ -39,9 +40,13 @@ Package.onUse(function (api) {
// console for debugging purposes
api.addFiles('log.js');
api.addFiles('both/collections.js', [ 'client', 'server' ]);
api.addFiles('both/schema.js', [ 'client', 'server' ]);
api.addFiles('both/collections.js', ['client', 'server']);
api.addFiles('both/schema.js', ['client', 'server']);
api.addFiles('both/lib/getCurrentServer.js', ['client', 'server']);
// Client collections and subscriptions
api.addFiles('client/collections/subscriptions.js', 'client');
// Components
api.addFiles('client/components/worklist.html', 'client');
api.addFiles('client/components/worklist.js', 'client');
@ -80,7 +85,24 @@ Package.onUse(function (api) {
api.addFiles('client/components/seriesDetailsTable/seriesDetailsTable.html', 'client');
api.addFiles('client/components/seriesDetailsTable/seriesDetailsTable.styl', 'client');
api.addFiles('client/components/seriesDetailsTable/seriesDetailsTable.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationDicomWeb/serverInformationDicomWeb.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationDicomWeb/serverInformationDicomWeb.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationDimse/serverInformationDimse.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationDimse/serverInformationDimse.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationForm.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationForm.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationForm/serverInformationFormField.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationList/serverInformationList.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationList/serverInformationList.js', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.html', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.styl', 'client');
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
// Client-side library functions
api.addFiles('client/lib/getStudyMetadata.js', 'client');
api.addFiles('client/lib/getStudiesMetadata.js', 'client');
@ -95,6 +117,7 @@ Package.onUse(function (api) {
// Server-side functions
api.addFiles('server/publications.js', 'server');
api.addFiles('server/validateServerConfiguration.js', 'server');
api.addFiles('server/servers.js', 'server');
api.addFiles('server/lib/remoteGetValue.js', 'server');
api.addFiles('server/lib/encodeQueryData.js', 'server');
@ -113,12 +136,20 @@ Package.onUse(function (api) {
api.addFiles('server/services/dimse/instances.js', 'server');
api.addFiles('server/services/dimse/studies.js', 'server');
api.addFiles('server/services/dimse/retrieveMetadata.js', 'server');
api.addFiles('server/services/dimse/setup.js', 'server');
// Study, instance, and metadata retrieval from remote PACS via Orthanc as a proxy
api.addFiles('server/services/remote/instances.js', 'server');
api.addFiles('server/services/remote/studies.js', 'server');
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
// Export Servers and CurrentServer Collections
api.export('Servers', ['client', 'server']);
api.export('CurrentServer', ['client', 'server']);
// Export shared lib functions
api.export('getCurrentServer', ['client', 'server']);
api.export('Services', 'server');
// Export Worklist helper functions for usage in Routes
@ -136,4 +167,4 @@ Package.onUse(function (api) {
api.export('WorklistTabs', 'client');
api.export('WorklistStudies', 'client');
api.export('WorklistSelectedStudies', 'client')
});
});

View File

@ -6,16 +6,17 @@ Meteor.methods({
GetStudyMetadata: function(studyInstanceUid) {
log.info('GetStudyMetadata(%s)', studyInstanceUid);
if (!Meteor.settings.servers.dicomWeb) {
throw 'No properly configured server was available over DICOMWeb';
// Get the server data. This is user-defined in the config.json files or through servers
// configuration modal
const server = getCurrentServer();
if (!server) {
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
}
if (Meteor.settings.servers.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
// Get the server data. This is user-defined in the
// config.json files used to run the Meteor server
var server = Meteor.settings.servers.dicomWeb[0];
if (server.type === 'dicomWeb') {
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
} else if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
} else if (server.type === 'dimse') {
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
}
}

View File

@ -39,7 +39,8 @@ Meteor.methods({
* @returns {boolean}
*/
importSupported: function() {
if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
const server = getCurrentServer();
if (server && server.type === 'dimse') {
return true;
}
},
@ -53,13 +54,17 @@ Meteor.methods({
return;
}
if (Meteor.settings.servers.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
const server = getCurrentServer();
if (!server) {
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
}
if (server.type === 'dicomWeb') {
//TODO: Support importing studies into dicomWeb
console.log('Importing studies into dicomWeb is currently not supported.');
} else if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
} else if (server.type === 'dimse') {
importStudiesDIMSE(studiesToImport, studyImportStatusId);
} else {
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
}
},
/**

View File

@ -1,19 +1,22 @@
Meteor.methods({
/**
* Use the specified filter to conduct a search from the DICOM server
*
* @param filter
*/
WorklistSearch: function(filter) {
if (Meteor.settings.servers.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
// Get the server data. This is user-defined in the
// config.json files used to run the Meteor server
var server = Meteor.settings.servers.dicomWeb[0];
WorklistSearch(filter) {
// Get the server data. This is user-defined in the config.json files or through servers
// configuration modal
const server = getCurrentServer();
return Services.QIDO.Studies(server, filter);
} else if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
return Services.DIMSE.Studies(filter);
} else {
if (!server) {
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
}
if (server.type === 'dicomWeb') {
return Services.QIDO.Studies(server, filter);
} else if (server.type === 'dimse') {
return Services.DIMSE.Studies(filter);
}
}
});

View File

@ -1,3 +1,11 @@
Meteor.publish('studyImportStatus', () => {
return StudyImportStatus.find();
});
});
Meteor.publish('servers', () => {
return Servers.find();
});
Meteor.publish('currentServer', () => {
return CurrentServer.find();
});

View File

@ -1,7 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
Meteor.startup(function() {
Servers.remove({
origin: 'json'
});
_.each(Meteor.settings.servers, function(endpoints, serverType) {
_.each(endpoints, function(endpoint) {
var server = _.clone(endpoint);
@ -10,28 +13,46 @@ Meteor.startup(function() {
Servers.insert(server);
});
});
ServersControl.resetCurrentServer();
});
class ServersControl {
static validateUser() {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized');
}
}
static writeCallback(error, affected) {
if (error) {
throw new Meteor.Error('data-write', error);
}
}
static resetCurrentServer() {
const currentServer = CurrentServer.findOne();
if (currentServer && Servers.find({ _id: currentServer.serverId }).count()) {
return;
}
const newServer = Servers.findOne({
origin: 'json',
type: Meteor.settings.defaultServiceType || 'dicomWeb'
});
if (newServer) {
CurrentServer.remove({});
CurrentServer.insert({
serverId: newServer._id
});
}
}
static find(query) {
return Servers.find(query).fetch();
}
static save(serverSettings) {
this.validateUser();
var query = {
const query = {
_id: serverSettings._id
};
var options = {
const options = {
upsert: true
};
@ -43,29 +64,28 @@ class ServersControl {
}
static setActive(serverId) {
this.validateUser();
var query = {
_id: Meteor.userId()
};
var data = {
$set: {
'profile.activeServer': serverId
}
};
Accounts.users.update(query, data, this.writeCallback);
CurrentServer.remove({});
CurrentServer.insert({
serverId: serverId
});
}
static remove(serverId) {
this.validateUser();
var query = {
_id: serverId
};
return Servers.remove(query, this.writeCallback);
const removeStatus = Servers.remove(query, this.writeCallback);
ServersControl.resetCurrentServer();
return removeStatus;
}
}
Meteor.methods({
serverFind: query => ServersControl.find(query),
serverSave: serverSettings => ServersControl.save(serverSettings),
serverSetActive: serverId => ServersControl.setActive(serverId),
serverRemove: serverId => ServersControl.remove(serverId)

View File

@ -1,68 +1,66 @@
/**
* Parses data returned from a study search and transforms it into
* an array of series that are present in the study
*
* @param resultData
* @returns {Array} Series List
*/
function resultDataToStudyMetadata(resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instanceRaw) {
var instance = instanceRaw.toObject();
// Use seriesMap to cache series data
// If the series instance UID has already been used to
// process series data, continue using that series
var seriesInstanceUid = instance[0x0020000E];
var series = seriesMap[seriesInstanceUid];
// If no series data exists in the seriesMap cache variable,
// process any available series data
if (!series) {
series = {
seriesInstanceUid: seriesInstanceUid,
seriesNumber: instance[0x00200011],
instances: []
};
// Save this data in the seriesMap cache variable
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
// TODO: [custom-servers] Check which peer it should point to
var server = Meteor.settings.servers.dimse[0].peers[0];
var host = Meteor.settings.servers.dimse.host,
port = Meteor.settings.servers.dimse.port;
var serverRoot = host + ':' + port;
var sopInstanceUid = instance[0x00080018];
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
// Add this instance to the current series
series.instances.push({
sopClassUid: instance[0x00080016],
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: instance[0x00200013]
});
});
return seriesList;
}
/**
* Retrieve a set of instances using a DIMSE call
* @param studyInstanceUid
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
Services.DIMSE.Instances = function(studyInstanceUid) {
//var url = buildUrl(server, studyInstanceUid);
var result = DIMSE.retrieveInstances(studyInstanceUid);
return {
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(result)
};
};
/**
* Parses data returned from a study search and transforms it into
* an array of series that are present in the study
*
* @param resultData
* @returns {Array} Series List
*/
function resultDataToStudyMetadata(resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instanceRaw) {
var instance = instanceRaw.toObject();
// Use seriesMap to cache series data
// If the series instance UID has already been used to
// process series data, continue using that series
var seriesInstanceUid = instance[0x0020000E];
var series = seriesMap[seriesInstanceUid];
// If no series data exists in the seriesMap cache variable,
// process any available series data
if (!series) {
series = {
seriesInstanceUid: seriesInstanceUid,
seriesNumber: instance[0x00200011],
instances: []
};
// Save this data in the seriesMap cache variable
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
// TODO: Check which peer it should point to
var server = getCurrentServer().peers[0];
var serverRoot = server.host + ':' + server.port;
var sopInstanceUid = instance[0x00080018];
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
// Add this instance to the current series
series.instances.push({
sopClassUid: instance[0x00080016],
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: instance[0x00200013]
});
});
return seriesList;
}
/**
* Retrieve a set of instances using a DIMSE call
* @param studyInstanceUid
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
Services.DIMSE.Instances = function(studyInstanceUid) {
//var url = buildUrl(server, studyInstanceUid);
var result = DIMSE.retrieveInstances(studyInstanceUid);
return {
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(result)
};
};

View File

@ -110,8 +110,8 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
};
// Retrieve the actual data over WADO-URI
var server = Meteor.settings.servers.dicomWeb[0];
instanceSummary.wadouri = WADOProxy.convertURL(server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom", server.requestOptions);
var server = getCurrentServer();
instanceSummary.wadouri = WADOProxy.convertURL(server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom', server.requestOptions);
series.instances.push(instanceSummary);
});
@ -127,8 +127,8 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
Services.DIMSE.RetrieveMetadata = function(studyInstanceUid) {
// TODO: Find active server
const activeServer = Meteor.settings.servers.dimse[0].peers[0];
// TODO: Check which peer it should point to
const activeServer = getCurrentServer().peers[0];
const supportsInstanceRetrievalByStudyUid = activeServer.supportsInstanceRetrievalByStudyUid;
let results;

View File

@ -0,0 +1,38 @@
import { Meteor } from 'meteor/meteor';
const setupDIMSE = () => {
// Terminate existing DIMSE servers and sockets and clean up the connection object
DIMSE.connection.reset();
// Get the new server configuration
const server = getCurrentServer();
// Stop here if the new server is not of DIMSE type
if (server.type !== 'dimse') {
return;
}
// Check if peers were defined in the server configuration and throw an error if not
const peers = server.peers;
if (!peers || !peers.length) {
console.error('dimse-config: ' + 'No DIMSE Peers provided.');
throw new Meteor.Error('dimse-config', 'No DIMSE Peers provided.');
}
// Add all the DIMSE peers, establishing the connections
console.log('Adding DIMSE peers');
try {
peers.forEach(peer => DIMSE.connection.addPeer(peer));
} catch(error) {
console.error('dimse-addPeers: ' + error);
throw new Meteor.Error('dimse-addPeers', error);
}
};
// Setup the DIMSE connections on startup or when the current server is changed
Meteor.startup(() => {
CurrentServer.find().observe({
added: setupDIMSE,
changed: setupDIMSE
});
});

View File

@ -17,6 +17,7 @@
}],
"dimse": [{
"name": "DCM4CHEE_DIMSE",
"wadoUriRoot": "http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado",
"peers": [
{
"host": "localhost",

View File

@ -18,6 +18,7 @@
],
"dimse": [{
"name": "OrthancDIMSE",
"wadoUriRoot" : "http://52.87.58.190:8043/wado",
"peers": [
{
"host" : "localhost",

View File

@ -17,6 +17,7 @@
],
"dimse" : [{
"name": "ORTHANC_DIMSE",
"wadoUriRoot": "http://dicom-orthanc.medken.com/wado",
"peers": [
{
"host": "208.100.41.100",

View File

@ -18,6 +18,7 @@
],
"dimse": [{
"name": "ORTHANC_DIMSE",
"wadoUriRoot": "http://localhost:8042/wado",
"peers": [
{
"host": "localhost",