Merged LesionTracker and OHIF Viewer, refactored out common components
This commit is contained in:
parent
34adbe3fbc
commit
b774c22593
12
LesionTracker/.meteor/.finished-upgraders
Normal file
12
LesionTracker/.meteor/.finished-upgraders
Normal file
@ -0,0 +1,12 @@
|
||||
# This file contains information which helps Meteor properly upgrade your
|
||||
# app when you run 'meteor update'. You should check it into version control
|
||||
# with your project.
|
||||
|
||||
notices-for-0.9.0
|
||||
notices-for-0.9.1
|
||||
0.9.4-platform-file
|
||||
notices-for-facebook-graph-api-2
|
||||
1.2.0-standard-minifiers-package
|
||||
1.2.0-meteor-platform-split
|
||||
1.2.0-cordova-changes
|
||||
1.2.0-breaking-changes
|
||||
1
LesionTracker/.meteor/.gitignore
vendored
Normal file
1
LesionTracker/.meteor/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
local
|
||||
7
LesionTracker/.meteor/.id
Normal file
7
LesionTracker/.meteor/.id
Normal file
@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
1q3vtk41m2yk4k1rqj08l
|
||||
34
LesionTracker/.meteor/packages
Normal file
34
LesionTracker/.meteor/packages
Normal file
@ -0,0 +1,34 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
# Check this file (and the other files in this directory) into your repository.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base # Packages every Meteor app needs to have
|
||||
mobile-experience # Packages for a great mobile UX
|
||||
mongo # The database Meteor supports right now
|
||||
blaze-html-templates # Compile .html files into Meteor Blaze views
|
||||
session # Client-side reactive dictionary for your app
|
||||
jquery # Helpful client-side library
|
||||
tracker # Meteor's client-side reactive programming library
|
||||
|
||||
standard-minifiers # JS/CSS minifiers run for production mode
|
||||
es5-shim # ECMAScript 5 compatibility for older browsers.
|
||||
ecmascript # Enable ECMAScript2015+ syntax in app code
|
||||
|
||||
autopublish # Publish all data to the clients (for prototyping)
|
||||
insecure # Allow all DB writes from clients (for prototyping)
|
||||
cornerstone
|
||||
worklist
|
||||
viewerbase
|
||||
dicomweb
|
||||
accounts-password
|
||||
|
||||
ian:accounts-ui-bootstrap-3
|
||||
iron:router
|
||||
twbs:bootstrap
|
||||
http
|
||||
arsnebula:reactive-promise
|
||||
promise
|
||||
stylus
|
||||
fortawesome:fontawesome
|
||||
2
LesionTracker/.meteor/platforms
Normal file
2
LesionTracker/.meteor/platforms
Normal file
@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
1
LesionTracker/.meteor/release
Normal file
1
LesionTracker/.meteor/release
Normal file
@ -0,0 +1 @@
|
||||
METEOR@1.2.0.2
|
||||
96
LesionTracker/.meteor/versions
Normal file
96
LesionTracker/.meteor/versions
Normal file
@ -0,0 +1,96 @@
|
||||
accounts-base@1.2.1
|
||||
accounts-password@1.1.3
|
||||
anti:i18n@0.4.3
|
||||
arsnebula:reactive-promise@0.9.1
|
||||
autopublish@1.0.4
|
||||
autoupdate@1.2.3
|
||||
babel-compiler@5.8.24_1
|
||||
babel-runtime@0.1.4
|
||||
base64@1.0.4
|
||||
binary-heap@1.0.4
|
||||
blaze@2.1.3
|
||||
blaze-html-templates@1.0.1
|
||||
blaze-tools@1.0.4
|
||||
boilerplate-generator@1.0.4
|
||||
caching-compiler@1.0.0
|
||||
caching-html-compiler@1.0.2
|
||||
callback-hook@1.0.4
|
||||
check@1.0.6
|
||||
cornerstone@0.0.1
|
||||
ddp@1.2.2
|
||||
ddp-client@1.2.1
|
||||
ddp-common@1.2.1
|
||||
ddp-rate-limiter@1.0.0
|
||||
ddp-server@1.2.1
|
||||
deps@1.0.9
|
||||
dicomweb@0.0.1
|
||||
diff-sequence@1.0.1
|
||||
ecmascript@0.1.5
|
||||
ecmascript-collections@0.1.6
|
||||
ejson@1.0.7
|
||||
email@1.0.7
|
||||
es5-shim@4.1.13
|
||||
fastclick@1.0.7
|
||||
fortawesome:fontawesome@4.4.0
|
||||
geojson-utils@1.0.4
|
||||
hot-code-push@1.0.0
|
||||
html-tools@1.0.5
|
||||
htmljs@1.0.5
|
||||
http@1.1.1
|
||||
ian:accounts-ui-bootstrap-3@1.2.82
|
||||
id-map@1.0.4
|
||||
insecure@1.0.4
|
||||
iron:controller@1.0.12
|
||||
iron:core@1.0.11
|
||||
iron:dynamic-template@1.0.12
|
||||
iron:layout@1.0.12
|
||||
iron:location@1.0.11
|
||||
iron:middleware-stack@1.0.11
|
||||
iron:router@1.0.12
|
||||
iron:url@1.0.11
|
||||
jquery@1.11.4
|
||||
launch-screen@1.0.4
|
||||
livedata@1.0.15
|
||||
localstorage@1.0.5
|
||||
logging@1.0.8
|
||||
meteor@1.1.9
|
||||
meteor-base@1.0.1
|
||||
meteor-platform@1.2.3
|
||||
minifiers@1.1.7
|
||||
minimongo@1.0.10
|
||||
mobile-experience@1.0.1
|
||||
mobile-status-bar@1.0.6
|
||||
mongo@1.1.2
|
||||
mongo-id@1.0.1
|
||||
npm-bcrypt@0.7.8_2
|
||||
npm-mongo@1.4.39_1
|
||||
observe-sequence@1.0.7
|
||||
ordered-dict@1.0.4
|
||||
promise@0.5.0
|
||||
random@1.0.4
|
||||
rate-limit@1.0.0
|
||||
reactive-dict@1.1.2
|
||||
reactive-var@1.0.6
|
||||
reload@1.1.4
|
||||
retry@1.0.4
|
||||
routepolicy@1.0.6
|
||||
service-configuration@1.0.5
|
||||
session@1.1.1
|
||||
sha@1.0.4
|
||||
spacebars@1.0.7
|
||||
spacebars-compiler@1.0.7
|
||||
srp@1.0.4
|
||||
standard-app-packages@1.0.6
|
||||
standard-minifiers@1.0.1
|
||||
stylus@2.511.0_2
|
||||
templating@1.1.4
|
||||
templating-tools@1.0.0
|
||||
tracker@1.0.9
|
||||
twbs:bootstrap@3.3.5
|
||||
ui@1.0.8
|
||||
underscore@1.0.4
|
||||
url@1.0.5
|
||||
viewerbase@0.0.1
|
||||
webapp@1.2.2
|
||||
webapp-hashing@1.0.5
|
||||
worklist@0.0.1
|
||||
2
LesionTracker/bin/localhostOrthanc.sh
Executable file
2
LesionTracker/bin/localhostOrthanc.sh
Executable file
@ -0,0 +1,2 @@
|
||||
echo "Starting Meteor server..."
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
|
||||
1
LesionTracker/bin/medicalConnections.sh
Executable file
1
LesionTracker/bin/medicalConnections.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
|
||||
1
LesionTracker/bin/medkenOrthanc.sh
Executable file
1
LesionTracker/bin/medkenOrthanc.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
|
||||
1
LesionTracker/bin/siimDCM4CHEE.sh
Executable file
1
LesionTracker/bin/siimDCM4CHEE.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
|
||||
33
LesionTracker/client/compatibility/docking-container.js
Normal file
33
LesionTracker/client/compatibility/docking-container.js
Normal file
@ -0,0 +1,33 @@
|
||||
//Collapse the container
|
||||
function collapseContainer ( container ) {
|
||||
container.toggleClass("collapseHorizontal");
|
||||
}
|
||||
|
||||
//function add collapse button
|
||||
function addButtonCollapse ( container ) {
|
||||
var btnCollapse = document.createElement("button");
|
||||
btnCollapse.innerHTML = ">";
|
||||
btnCollapse.classList.add('btnCollapse');
|
||||
|
||||
btnCollapse.onclick = function() { // Note this is a function
|
||||
collapseContainer(container);
|
||||
};
|
||||
container.append(btnCollapse);
|
||||
}
|
||||
|
||||
$.fn.dockingContainer = function(options) {
|
||||
var self = this;
|
||||
var opts = $.extend( {}, $.fn.dockingContainer.defaults, options);
|
||||
|
||||
//Set container class
|
||||
this.addClass("dockingContainer");
|
||||
|
||||
//Add btnCollapse
|
||||
addButtonCollapse(this);
|
||||
return this;
|
||||
};
|
||||
|
||||
//Defaults
|
||||
$.fn.dockingContainer.defaults = {
|
||||
background: "black"
|
||||
};
|
||||
285
LesionTracker/client/compatibility/lesionTool.js
Normal file
285
LesionTracker/client/compatibility/lesionTool.js
Normal file
@ -0,0 +1,285 @@
|
||||
var lineIndex = 0; //This holds drawn line index
|
||||
var cornerstoneTools = (function ($, cornerstone, cornerstoneMath, cornerstoneTools) {
|
||||
|
||||
"use strict";
|
||||
|
||||
if(cornerstoneTools === undefined) {
|
||||
cornerstoneTools = {};
|
||||
}
|
||||
|
||||
var toolType = "lesion";
|
||||
|
||||
///////// BEGIN ACTIVE TOOL ///////
|
||||
function createNewMeasurement(mouseEventData)
|
||||
{
|
||||
var element = mouseEventData.element;
|
||||
var lesionCounter = "";
|
||||
|
||||
//setting name of lesion
|
||||
$(element).on("LesionNameSet", function(e,counter){
|
||||
lesionCounter = counter;
|
||||
});
|
||||
|
||||
//Listens LesionToolModified event and calls measurementModified function when lesion measurement is changed or updated.
|
||||
$(element).on("LesionToolModified", measurementModified);
|
||||
$(element).trigger("LesionMeasurementCreated");
|
||||
|
||||
// create the measurement data for this tool with the end handle activated
|
||||
var measurementData = {
|
||||
visible : true,
|
||||
active : true,
|
||||
handles : {
|
||||
start : {
|
||||
x : mouseEventData.currentPoints.image.x,
|
||||
y : mouseEventData.currentPoints.image.y,
|
||||
highlight: true,
|
||||
active: false
|
||||
},
|
||||
end: {
|
||||
x : mouseEventData.currentPoints.image.x,
|
||||
y : mouseEventData.currentPoints.image.y,
|
||||
highlight: true,
|
||||
active: true
|
||||
}
|
||||
},
|
||||
index: lineIndex,
|
||||
measurementText: "",
|
||||
linkedTextCoords: {
|
||||
start : {
|
||||
x : mouseEventData.currentPoints.image.x,
|
||||
y : mouseEventData.currentPoints.image.y,
|
||||
highlight: true,
|
||||
active: false
|
||||
},
|
||||
end: {
|
||||
x : mouseEventData.currentPoints.image.x,
|
||||
y : mouseEventData.currentPoints.image.y,
|
||||
highlight: true,
|
||||
active: true
|
||||
},
|
||||
init: false
|
||||
},
|
||||
lesionName: "Target "+lesionCounter,
|
||||
isDeleted: false,
|
||||
lineNumber:"" //Indicates line number on image
|
||||
};
|
||||
lineIndex++;
|
||||
return measurementData;
|
||||
}
|
||||
///////// END ACTIVE TOOL ///////
|
||||
|
||||
function pointNearTool(element, data, coords)
|
||||
{
|
||||
var lineSegment = {
|
||||
start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.end)
|
||||
};
|
||||
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
|
||||
return (distanceToPoint < 25);
|
||||
}
|
||||
|
||||
function pointNearToolForText(element, data, coords) {
|
||||
var lineSegment = {
|
||||
start: cornerstone.pixelToCanvas(element, data.linkedTextCoords.start), end: cornerstone.pixelToCanvas(element, data.linkedTextCoords.end)
|
||||
};
|
||||
|
||||
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
|
||||
return (distanceToPoint < 30);
|
||||
|
||||
}
|
||||
|
||||
|
||||
///////// BEGIN IMAGE RENDERING ///////
|
||||
function onImageRendered(e, eventData) {
|
||||
|
||||
// if we have no toolData for this element, return immediately as there is nothing to do
|
||||
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
|
||||
if (toolData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have tool data for this element - iterate over each one and draw it
|
||||
var context = eventData.canvasContext.canvas.getContext('2d');
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
|
||||
var color;
|
||||
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
|
||||
var font = cornerstoneTools.textStyle.getFont();
|
||||
var config = cornerstoneTools.length.getConfiguration();
|
||||
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
context.save();
|
||||
// configurable shadow
|
||||
if (config && config.shadow) {
|
||||
context.shadowColor = '#000000';
|
||||
context.shadowOffsetX = +1;
|
||||
context.shadowOffsetY = +1;
|
||||
}
|
||||
|
||||
var data = toolData.data[i];
|
||||
if (data.active) {
|
||||
color = cornerstoneTools.toolColors.getActiveColor();
|
||||
} else {
|
||||
color = cornerstoneTools.toolColors.getToolColor();
|
||||
}
|
||||
|
||||
// draw the line
|
||||
var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
|
||||
var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
|
||||
|
||||
context.beginPath();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
|
||||
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
|
||||
context.stroke();
|
||||
|
||||
// draw the handles
|
||||
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
|
||||
|
||||
//Set coordinates of text
|
||||
var linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
|
||||
var linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
|
||||
if(!data.linkedTextCoords.init){
|
||||
data.linkedTextCoords.start.x = data.handles.start.x + 50;
|
||||
data.linkedTextCoords.start.y = data.handles.start.y + 40;
|
||||
linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
|
||||
|
||||
//Set end point of linkedTextCoords
|
||||
data.linkedTextCoords.end.x = data.linkedTextCoords.start.x + 50;
|
||||
data.linkedTextCoords.end.y = data.linkedTextCoords.start.y + 30;
|
||||
linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
|
||||
|
||||
//initialized coordinates of text
|
||||
data.linkedTextCoords.init = true;
|
||||
}
|
||||
|
||||
//Draw linked line as dashed
|
||||
context.setLineDash([2,3]);
|
||||
context.beginPath();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1 / eventData.viewport.scale;
|
||||
var mid = {
|
||||
x:(handleStartCanvas.x + handleEndCanvas.x) / 2, y:(handleStartCanvas.y + handleEndCanvas.y) / 2
|
||||
};
|
||||
|
||||
context.moveTo(mid.x, mid.y);
|
||||
context.lineTo(linkedTextStartCanvas.x + 20, linkedTextStartCanvas.y + 20);
|
||||
context.stroke();
|
||||
|
||||
// Draw the text
|
||||
context.fillStyle = color;
|
||||
context.font = font;
|
||||
var dx = (data.handles.start.x - data.handles.end.x) * (eventData.image.columnPixelSpacing || 1);
|
||||
var dy = (data.handles.start.y - data.handles.end.y) * (eventData.image.rowPixelSpacing || 1);
|
||||
var length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
var suffix = ' mm';
|
||||
if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
|
||||
suffix = ' pixels';
|
||||
}
|
||||
|
||||
var text = '' + length.toFixed(2) + suffix;
|
||||
var textCoords = {
|
||||
x: linkedTextStartCanvas.x, y: linkedTextStartCanvas.y
|
||||
};
|
||||
|
||||
cornerstoneTools.drawTextBox(context, data.lesionName, textCoords.x, textCoords.y, color);
|
||||
cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y + 20, color);
|
||||
|
||||
//Set measurement text to show lesion table
|
||||
data.measurementText = length.toFixed(2);
|
||||
$(eventData.enabledElement.element).trigger("LesionTextChanged",data);
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
}
|
||||
///////// END IMAGE RENDERING ///////
|
||||
|
||||
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
|
||||
function measurementModified(e, eventObject){
|
||||
var start = new Date();
|
||||
|
||||
var enabledElement = eventObject.enabledElement;
|
||||
var lineIndex = eventObject.lineIndex;
|
||||
var type = eventObject.type;
|
||||
|
||||
// if we have no toolData for this element, return immediately as there is nothing to do
|
||||
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
|
||||
if (toolData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
//If type is delete, remove measurement
|
||||
if(type === "delete") {
|
||||
var deletedDataIndex = -1;
|
||||
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
var data = toolData.data[i];
|
||||
|
||||
//When click a row of table measurements, measurement will be active and color will be green
|
||||
if (data.index === eventObject.lineIndex && eventObject.type !== "active") {
|
||||
data.visible = false;
|
||||
deletedDataIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
|
||||
toolData.data.splice(deletedDataIndex, 1);
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
var data = toolData.data[i];
|
||||
|
||||
//When click a row of table measurements, measurement will be active and color will be green
|
||||
if (data.index === eventObject.lineIndex && eventObject.type === "active") {
|
||||
data.active = true;
|
||||
} else{
|
||||
data.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enabledElement.image.render(enabledElement, true);
|
||||
|
||||
var context = enabledElement.canvas.getContext('2d');
|
||||
|
||||
var end = new Date();
|
||||
var diff = end - start;
|
||||
//console.log(diff + ' ms');
|
||||
|
||||
var eventData = {
|
||||
viewport : enabledElement.viewport,
|
||||
element : enabledElement.element,
|
||||
image : enabledElement.image,
|
||||
enabledElement : enabledElement,
|
||||
canvasContext: context,
|
||||
measurementText: "",
|
||||
renderTimeInMs : diff,
|
||||
lineIndex : lineIndex,
|
||||
type : type //Holds image will be deleted or active
|
||||
};
|
||||
|
||||
enabledElement.invalid = false;
|
||||
|
||||
onImageRendered(e, eventData);
|
||||
}
|
||||
|
||||
// module exports
|
||||
cornerstoneTools.lesion = cornerstoneTools.mouseButtonTool({
|
||||
createNewMeasurement : createNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool : pointNearTool,
|
||||
pointNearToolForText: pointNearToolForText,
|
||||
toolType : toolType
|
||||
});
|
||||
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
|
||||
createNewMeasurement: createNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool: pointNearTool,
|
||||
pointNearToolForText: pointNearToolForText,
|
||||
toolType: toolType
|
||||
});
|
||||
return cornerstoneTools;
|
||||
}($, cornerstone, cornerstoneMath, cornerstoneTools));
|
||||
15
LesionTracker/client/compatibility/tabHelper.js
Normal file
15
LesionTracker/client/compatibility/tabHelper.js
Normal file
@ -0,0 +1,15 @@
|
||||
//Holds functions to help managing tabs
|
||||
|
||||
//Returns activeTabId
|
||||
function activeTabId() {
|
||||
var activeTab = Session.get("activeTabId");
|
||||
return activeTab;
|
||||
}
|
||||
|
||||
//Returns activeTabContentId
|
||||
function activeTabContentId () {
|
||||
var activeTabId = activeTabId();
|
||||
var splitTabId = activeTabId.split("tab");
|
||||
var contentId = "content"+splitTabId[1];
|
||||
return contentId;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<template name="hidingPanel">
|
||||
<div class="hidingPanel">
|
||||
{{> studyBrowser}}
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,9 @@
|
||||
.hidingPanel
|
||||
width: 12%
|
||||
height: 100%
|
||||
margin-top: 35px
|
||||
position: relative
|
||||
float: left
|
||||
overflow: auto
|
||||
background-color: blue
|
||||
color: black
|
||||
48
LesionTracker/client/components/toolbar/toolbar.css
Normal file
48
LesionTracker/client/components/toolbar/toolbar.css
Normal file
@ -0,0 +1,48 @@
|
||||
.toolbar {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#toolbar-group button {
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
margin-right: 3px;
|
||||
margin-left: 3px;
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#toolbar-group button > span {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 9px;
|
||||
right: 9px;
|
||||
bottom: 9px;
|
||||
}
|
||||
|
||||
|
||||
#toolbar-group .btn:hover {
|
||||
border-width: 3px;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
#toolbar-group .btn:focus, .btn:active {
|
||||
outline: 0 !important;
|
||||
border-width: 5px;
|
||||
background-color: transparent;
|
||||
color:white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#toolbar-group .btn:focus > span{
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
}
|
||||
36
LesionTracker/client/components/toolbar/toolbar.html
Normal file
36
LesionTracker/client/components/toolbar/toolbar.html
Normal file
@ -0,0 +1,36 @@
|
||||
<template name="toolbar">
|
||||
<div id="toolbar" class="toolbar">
|
||||
<div id="toolbar-group" class="btn-group">
|
||||
<!-- WW/WL -->
|
||||
<button id="wwwc" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="WW/WC"><span class="fa fa-sun-o"></span></button>
|
||||
<!-- Invert -->
|
||||
<button id="invert" type="button" class="imageViewerCommand btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Invert"><span class="fa fa-adjust"></span></button>
|
||||
<!-- Zoom -->
|
||||
<button id="zoom" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Zoom"><span class="fa fa-search"></span></button>
|
||||
<!-- Pan -->
|
||||
<button id="pan" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pan"><span class="fa fa-arrows"></span></button>
|
||||
<!-- Stack scroll -->
|
||||
<button id="stackScroll" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Stack Scroll"><span class="fa fa-bars"></span></button>
|
||||
<!-- Play clip -->
|
||||
<button id="playClip" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Play/Stop Clip">
|
||||
{{ #if isPlaying }}
|
||||
<span class="fa fa-stop"></span>
|
||||
{{ else }}
|
||||
<span class="fa fa-play"></span>
|
||||
{{ /if }}
|
||||
</button>
|
||||
<!-- Angle measurement -->
|
||||
<!--<button id="angle" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Angle Measurement"><span class="fa fa-angle-left"></span></button>-->
|
||||
<!-- Pixel probe -->
|
||||
<button id="dragProbe" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pixel Probe"><span class="fa fa-dot-circle-o"></span></button>
|
||||
<!-- Length measurement -->
|
||||
<button id="length" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Length Measurement"><span class="fa fa-arrows-v"></span></button>
|
||||
<!-- Elliptical ROI -->
|
||||
<button id="ellipticalRoi" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Elliptical ROI"><span class="fa fa-circle-o"></span></button>
|
||||
<!-- Rectangle ROI -->
|
||||
<button id="rectangleRoi" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Rectangle ROI"><span class="fa fa-square-o"></span></button>
|
||||
<!-- Lesion Tool -->
|
||||
<button id="lesion" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Lesion Tool"><span class="fa fa-arrows-v"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
LesionTracker/client/components/toolbar/toolbar.js
Normal file
34
LesionTracker/client/components/toolbar/toolbar.js
Normal file
@ -0,0 +1,34 @@
|
||||
Template.toolbar.events({
|
||||
'click .imageViewerTool': function(e) {
|
||||
var tool = e.currentTarget.id;
|
||||
console.log('Setting active tool to: ' + tool);
|
||||
toolManager.setActiveTool(tool);
|
||||
},
|
||||
'click .imageViewerCommand': function(e) {
|
||||
var command = e.currentTarget.id;
|
||||
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO = Add support for active viewports
|
||||
//var element = getActiveViewportElement();
|
||||
$('.imageViewer').each(function() {
|
||||
var element = this;
|
||||
OHIF.viewer.functionList[command](element);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbar.onRendered(function() {
|
||||
var tooltipButtons = $('[data-toggle="tooltip"]');
|
||||
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
|
||||
// Enable tooltips for the layout button
|
||||
var extraTooltipButtons = $('[rel="tooltip"]');
|
||||
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
|
||||
toolManager.addTool('lesion', {
|
||||
mouse: cornerstoneTools.lesion,
|
||||
touch: cornerstoneTools.lesionTouch
|
||||
});
|
||||
});
|
||||
7
LesionTracker/client/components/viewer.css
Normal file
7
LesionTracker/client/components/viewer.css
Normal file
@ -0,0 +1,7 @@
|
||||
.viewer{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: black;
|
||||
}
|
||||
9
LesionTracker/client/components/viewer.html
Normal file
9
LesionTracker/client/components/viewer.html
Normal file
@ -0,0 +1,9 @@
|
||||
<template name="viewer">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
||||
</head>
|
||||
<div id="viewer" class="viewer">
|
||||
{{ >hidingPanel }}
|
||||
{{ >viewerMain }}
|
||||
</div>
|
||||
</template>
|
||||
89
LesionTracker/client/components/viewer.js
Normal file
89
LesionTracker/client/components/viewer.js
Normal file
@ -0,0 +1,89 @@
|
||||
OHIF = {
|
||||
viewer: {}
|
||||
};
|
||||
|
||||
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
|
||||
OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
|
||||
OHIF.viewer.loadIndicatorDelay = 3000;
|
||||
OHIF.viewer.defaultTool = 'wwwc';
|
||||
OHIF.viewer.refLinesEnabled = true;
|
||||
OHIF.viewer.isPlaying = {};
|
||||
|
||||
Meteor.startup(function() {
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
|
||||
OHIF.viewer.functionList = {
|
||||
invert: function(element) {
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(element, viewport);
|
||||
},
|
||||
playClip: function(element) {
|
||||
var viewportIndex = $('.imageViewerViewport').index(element);
|
||||
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
|
||||
if (isPlaying === true) {
|
||||
cornerstoneTools.stopClip(element);
|
||||
} else {
|
||||
cornerstoneTools.playClip(element);
|
||||
}
|
||||
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
|
||||
Session.set('UpdateCINE', Random.id());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isTouchDevice()) {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'manual'
|
||||
};
|
||||
} else {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'hover'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function resizeViewports() {
|
||||
// Handle resizing of image viewer viewports
|
||||
// For some reason, this seems to need to be on
|
||||
// another delay, or the resizing won't work properly
|
||||
viewportResizeTimer = setTimeout(function() {
|
||||
var elements = $('.imageViewerViewport');
|
||||
elements.each(function(index) {
|
||||
var element = this;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
cornerstone.resize(element, true);
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
|
||||
Session.setDefault('ActiveViewport', 0);
|
||||
Session.setDefault('viewportRows', 1);
|
||||
Session.setDefault('viewportColumns', 1);
|
||||
|
||||
// Avoid doing DOM manipulation during the resize handler
|
||||
// because it is fired very often.
|
||||
// Resizing is therefore performed 100 ms after the resize event stops.
|
||||
var resizeTimer;
|
||||
$(window).on('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
resizeViewports();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
var imageViewer = $("#viewer");
|
||||
if (imageViewer) {
|
||||
$('.navbar-default').css({
|
||||
'background-color': '#000000',
|
||||
'border-color': '#101010'
|
||||
});
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.height = '100%';
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.minWidth = 0;
|
||||
document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
.viewerMain {
|
||||
width: 88%;
|
||||
height: 100%;
|
||||
margin-left: 12%;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<template name="viewerMain">
|
||||
<div class="viewerMain">
|
||||
{{> toolbar}}
|
||||
{{> imageViewerViewports}}
|
||||
<!--TODO: Bottom lesion table container will be here-->
|
||||
</div>
|
||||
</template>
|
||||
12
LesionTracker/client/head.html
Normal file
12
LesionTracker/client/head.html
Normal file
@ -0,0 +1,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Lesion Tracker</title>
|
||||
<meta name="description" content="Prototype viewer application for oncology">
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />
|
||||
<link rel="shortcut icon" type="image/png" href="images/logo.png" sizes="16x16 32x32 64x64">
|
||||
<!--Best display for mobile devices-->
|
||||
<meta http-equiv="cleartype" content="on">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
</head>
|
||||
BIN
LesionTracker/client/images/logo.png
Normal file
BIN
LesionTracker/client/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
34
LesionTracker/client/route.js
Normal file
34
LesionTracker/client/route.js
Normal file
@ -0,0 +1,34 @@
|
||||
Router.configure({
|
||||
layoutTemplate: 'layout',
|
||||
//loadingTemplate: '',
|
||||
notFoundTemplate: 'notFound'
|
||||
});
|
||||
|
||||
|
||||
|
||||
Router.route('/', function () {
|
||||
this.render('worklist', {
|
||||
});
|
||||
});
|
||||
|
||||
ViewerStudies = new Mongo.Collection(null);
|
||||
|
||||
Router.route('/viewer/:_id', {
|
||||
layoutTemplate: 'layout',
|
||||
name: 'viewer',
|
||||
onBeforeAction: function() {
|
||||
var self = this;
|
||||
//Session.set('openNewTabEvent', self);
|
||||
|
||||
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
|
||||
//console.log(study);
|
||||
sortStudy(study);
|
||||
|
||||
var studies = [study];
|
||||
Session.set('studies', studies);
|
||||
//Session.set(activeTabId, studies);
|
||||
//Session.set('showContentInTab', true);
|
||||
self.render('viewer');
|
||||
});
|
||||
}
|
||||
});
|
||||
5
LesionTracker/client/routeHooks.js
Normal file
5
LesionTracker/client/routeHooks.js
Normal file
@ -0,0 +1,5 @@
|
||||
Router.onBeforeAction(function () {
|
||||
|
||||
// User is logged in, go ahead and route them
|
||||
this.next();
|
||||
});
|
||||
52
LesionTracker/client/stylesheets/docking-container.css
Normal file
52
LesionTracker/client/stylesheets/docking-container.css
Normal file
@ -0,0 +1,52 @@
|
||||
/*****************************/
|
||||
/* Toggle */
|
||||
/*****************************/
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dockingContainer, .collapseVertical, .collapseHorizontal {
|
||||
-moz-transition: all 0.3s ease-out;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.dockingContainer {
|
||||
-moz-transition: all 0.3s ease-out;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.collapseVertical {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.collapseHorizontal {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.btnCollapse{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
background: #f6f6f6;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.btnCollapseIcon-collapse{
|
||||
-moz-transform: rotate(180deg);
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
3
OHIFViewer/.gitignore
vendored
Normal file
3
OHIFViewer/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.meteor/local
|
||||
.meteor/meteorite
|
||||
node_modules
|
||||
12
OHIFViewer/.meteor/.finished-upgraders
Normal file
12
OHIFViewer/.meteor/.finished-upgraders
Normal file
@ -0,0 +1,12 @@
|
||||
# This file contains information which helps Meteor properly upgrade your
|
||||
# app when you run 'meteor update'. You should check it into version control
|
||||
# with your project.
|
||||
|
||||
notices-for-0.9.0
|
||||
notices-for-0.9.1
|
||||
0.9.4-platform-file
|
||||
notices-for-facebook-graph-api-2
|
||||
1.2.0-standard-minifiers-package
|
||||
1.2.0-meteor-platform-split
|
||||
1.2.0-cordova-changes
|
||||
1.2.0-breaking-changes
|
||||
1
OHIFViewer/.meteor/.gitignore
vendored
Normal file
1
OHIFViewer/.meteor/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
local
|
||||
7
OHIFViewer/.meteor/.id
Normal file
7
OHIFViewer/.meteor/.id
Normal file
@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
1jfkhvr1ljfqodmpn32m
|
||||
34
OHIFViewer/.meteor/packages
Normal file
34
OHIFViewer/.meteor/packages
Normal file
@ -0,0 +1,34 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
# Check this file (and the other files in this directory) into your repository.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
accounts-password
|
||||
ian:accounts-ui-bootstrap-3
|
||||
iron:router
|
||||
twbs:bootstrap
|
||||
http
|
||||
arsnebula:reactive-promise
|
||||
promise
|
||||
stylus
|
||||
fortawesome:fontawesome
|
||||
mrt:moment
|
||||
standard-minifiers
|
||||
meteor-base
|
||||
mobile-experience
|
||||
mongo
|
||||
blaze-html-templates
|
||||
session
|
||||
jquery
|
||||
tracker
|
||||
logging
|
||||
reload
|
||||
random
|
||||
ejson
|
||||
spacebars
|
||||
check
|
||||
cornerstone
|
||||
dicomweb
|
||||
viewerbase
|
||||
worklist
|
||||
2
OHIFViewer/.meteor/platforms
Normal file
2
OHIFViewer/.meteor/platforms
Normal file
@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
1
OHIFViewer/.meteor/release
Normal file
1
OHIFViewer/.meteor/release
Normal file
@ -0,0 +1 @@
|
||||
METEOR@1.2.0.2
|
||||
94
OHIFViewer/.meteor/versions
Normal file
94
OHIFViewer/.meteor/versions
Normal file
@ -0,0 +1,94 @@
|
||||
accounts-base@1.2.1
|
||||
accounts-password@1.1.3
|
||||
anti:i18n@0.4.3
|
||||
arsnebula:reactive-promise@0.9.1
|
||||
autoupdate@1.2.3
|
||||
babel-compiler@5.8.24_1
|
||||
babel-runtime@0.1.4
|
||||
base64@1.0.4
|
||||
binary-heap@1.0.4
|
||||
blaze@2.1.3
|
||||
blaze-html-templates@1.0.1
|
||||
blaze-tools@1.0.4
|
||||
boilerplate-generator@1.0.4
|
||||
caching-compiler@1.0.0
|
||||
caching-html-compiler@1.0.2
|
||||
callback-hook@1.0.4
|
||||
check@1.0.6
|
||||
cornerstone@0.0.1
|
||||
ddp@1.2.2
|
||||
ddp-client@1.2.1
|
||||
ddp-common@1.2.1
|
||||
ddp-rate-limiter@1.0.0
|
||||
ddp-server@1.2.1
|
||||
deps@1.0.9
|
||||
dicomweb@0.0.1
|
||||
diff-sequence@1.0.1
|
||||
ecmascript@0.1.5
|
||||
ecmascript-collections@0.1.6
|
||||
ejson@1.0.7
|
||||
email@1.0.7
|
||||
fastclick@1.0.7
|
||||
fortawesome:fontawesome@4.4.0
|
||||
geojson-utils@1.0.4
|
||||
hot-code-push@1.0.0
|
||||
html-tools@1.0.5
|
||||
htmljs@1.0.5
|
||||
http@1.1.1
|
||||
ian:accounts-ui-bootstrap-3@1.2.81
|
||||
id-map@1.0.4
|
||||
iron:controller@1.0.8
|
||||
iron:core@1.0.8
|
||||
iron:dynamic-template@1.0.8
|
||||
iron:layout@1.0.8
|
||||
iron:location@1.0.9
|
||||
iron:middleware-stack@1.0.9
|
||||
iron:router@1.0.9
|
||||
iron:url@1.0.9
|
||||
jquery@1.11.4
|
||||
launch-screen@1.0.4
|
||||
livedata@1.0.15
|
||||
localstorage@1.0.5
|
||||
logging@1.0.8
|
||||
meteor@1.1.9
|
||||
meteor-base@1.0.1
|
||||
meteor-platform@1.2.3
|
||||
minifiers@1.1.7
|
||||
minimongo@1.0.10
|
||||
mobile-experience@1.0.1
|
||||
mobile-status-bar@1.0.6
|
||||
mongo@1.1.2
|
||||
mongo-id@1.0.1
|
||||
mrt:moment@2.8.1
|
||||
npm-bcrypt@0.7.8_2
|
||||
npm-mongo@1.4.39_1
|
||||
observe-sequence@1.0.7
|
||||
ordered-dict@1.0.4
|
||||
promise@0.5.0
|
||||
random@1.0.4
|
||||
rate-limit@1.0.0
|
||||
reactive-dict@1.1.2
|
||||
reactive-var@1.0.6
|
||||
reload@1.1.4
|
||||
retry@1.0.4
|
||||
routepolicy@1.0.6
|
||||
service-configuration@1.0.5
|
||||
session@1.1.1
|
||||
sha@1.0.4
|
||||
spacebars@1.0.7
|
||||
spacebars-compiler@1.0.7
|
||||
srp@1.0.4
|
||||
standard-app-packages@1.0.6
|
||||
standard-minifiers@1.0.1
|
||||
stylus@2.511.0_2
|
||||
templating@1.1.4
|
||||
templating-tools@1.0.0
|
||||
tracker@1.0.9
|
||||
twbs:bootstrap@3.3.5
|
||||
ui@1.0.8
|
||||
underscore@1.0.4
|
||||
url@1.0.5
|
||||
viewerbase@0.0.1
|
||||
webapp@1.2.2
|
||||
webapp-hashing@1.0.5
|
||||
worklist@0.0.1
|
||||
2
OHIFViewer/bin/localhostOrthanc.sh
Executable file
2
OHIFViewer/bin/localhostOrthanc.sh
Executable file
@ -0,0 +1,2 @@
|
||||
echo "Starting Meteor server..."
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
|
||||
1
OHIFViewer/bin/medicalConnections.sh
Executable file
1
OHIFViewer/bin/medicalConnections.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
|
||||
1
OHIFViewer/bin/medkenOrthanc.sh
Executable file
1
OHIFViewer/bin/medkenOrthanc.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
|
||||
1
OHIFViewer/bin/siimDCM4CHEE.sh
Executable file
1
OHIFViewer/bin/siimDCM4CHEE.sh
Executable file
@ -0,0 +1 @@
|
||||
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
|
||||
36
OHIFViewer/client/components/viewer/toolRow/toolbar.html
Normal file
36
OHIFViewer/client/components/viewer/toolRow/toolbar.html
Normal file
@ -0,0 +1,36 @@
|
||||
<template name="toolbar">
|
||||
<div id='toolbar'>
|
||||
<div class="btn-group">
|
||||
<!-- WW/WL -->
|
||||
<button id="wwwc" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="WW/WC"><span class="fa fa-sun-o"></span></button>
|
||||
<!-- Invert -->
|
||||
<button id="invert" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Invert"><span class="fa fa-adjust"></span></button>
|
||||
<!-- Zoom -->
|
||||
<button id="zoom" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Zoom"><span class="fa fa-search"></span></button>
|
||||
<!-- Pan -->
|
||||
<button id="pan" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pan"><span class="fa fa-arrows"></span></button>
|
||||
<!-- Stack scroll -->
|
||||
<button id="stackScroll" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Stack Scroll"><span class="fa fa-bars"></span></button>
|
||||
<!-- Length measurement -->
|
||||
<button id="length" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Length Measurement"><span class="fa fa-arrows-v"></span></button>
|
||||
<!-- Angle measurement -->
|
||||
<button id="angle" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Angle Measurement"><span class="fa fa-angle-left"></span></button>
|
||||
<!-- Pixel probe -->
|
||||
<button id="dragProbe" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pixel Probe"><span class="fa fa-dot-circle-o"></span></button>
|
||||
<!-- Elliptical ROI -->
|
||||
<button id="ellipticalRoi" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Elliptical ROI"><span class="fa fa-circle-o"></span></button>
|
||||
<!-- Rectangle ROI -->
|
||||
<button id="rectangleRoi" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Rectangle ROI"><span class="fa fa-square-o"></span></button>
|
||||
<!-- Play clip -->
|
||||
<button id="playClip" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Play/Stop Clip">
|
||||
{{ #if isPlaying }}
|
||||
<span class="fa fa-stop"></span>
|
||||
{{ else }}
|
||||
<span class="fa fa-play"></span>
|
||||
{{ /if }}
|
||||
</button>
|
||||
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip"><span class="fa fa-th-large"></span></button>
|
||||
{{> layoutChooser}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
40
OHIFViewer/client/components/viewer/toolRow/toolbar.js
Normal file
40
OHIFViewer/client/components/viewer/toolRow/toolbar.js
Normal file
@ -0,0 +1,40 @@
|
||||
Template.toolbar.helpers({
|
||||
studies : function() {
|
||||
var studies = Session.get('studies');
|
||||
return studies;
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbar.events({
|
||||
'click .imageViewerTool': function(e) {
|
||||
var tool = e.currentTarget.id;
|
||||
console.log('Setting active tool to: ' + tool);
|
||||
toolManager.setActiveTool(tool);
|
||||
},
|
||||
'click .imageViewerCommand': function(e) {
|
||||
var command = e.currentTarget.id;
|
||||
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
|
||||
return;
|
||||
}
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
OHIF.viewer.functionList[command](element);
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbar.onRendered(function() {
|
||||
var tooltipButtons = $('[data-toggle="tooltip"]');
|
||||
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
|
||||
// Enable tooltips for the layout button
|
||||
var extraTooltipButtons = $('[rel="tooltip"]');
|
||||
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
});
|
||||
|
||||
Template.toolbar.helpers({
|
||||
'isPlaying': function() {
|
||||
Session.get('UpdateCINE');
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
return !!OHIF.viewer.isPlaying[viewportIndex];
|
||||
}
|
||||
});
|
||||
15
OHIFViewer/client/components/viewer/toolRow/toolbar.styl
Normal file
15
OHIFViewer/client/components/viewer/toolRow/toolbar.styl
Normal file
@ -0,0 +1,15 @@
|
||||
#toolbar
|
||||
background-color: black
|
||||
text-align: center
|
||||
|
||||
.btn-group
|
||||
display: inline-block
|
||||
|
||||
button
|
||||
text-align: center
|
||||
color: #ffffff
|
||||
background-color: #424242
|
||||
border-color: #424242
|
||||
|
||||
&.active, &:active
|
||||
background-color: #3B678E
|
||||
9
OHIFViewer/client/components/viewer/viewer.html
Normal file
9
OHIFViewer/client/components/viewer/viewer.html
Normal file
@ -0,0 +1,9 @@
|
||||
<template name="viewer">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
||||
</head>
|
||||
<div id="viewer">
|
||||
{{> studyBrowser}}
|
||||
{{> viewerMain}}
|
||||
</div>
|
||||
</template>
|
||||
89
OHIFViewer/client/components/viewer/viewer.js
Normal file
89
OHIFViewer/client/components/viewer/viewer.js
Normal file
@ -0,0 +1,89 @@
|
||||
OHIF = {
|
||||
viewer: {}
|
||||
};
|
||||
|
||||
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
|
||||
OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
|
||||
OHIF.viewer.loadIndicatorDelay = 3000;
|
||||
OHIF.viewer.defaultTool = 'wwwc';
|
||||
OHIF.viewer.refLinesEnabled = true;
|
||||
OHIF.viewer.isPlaying = {};
|
||||
|
||||
Meteor.startup(function() {
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
|
||||
OHIF.viewer.functionList = {
|
||||
invert: function(element) {
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(element, viewport);
|
||||
},
|
||||
playClip: function(element) {
|
||||
var viewportIndex = $('.imageViewerViewport').index(element);
|
||||
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
|
||||
if (isPlaying === true) {
|
||||
cornerstoneTools.stopClip(element);
|
||||
} else {
|
||||
cornerstoneTools.playClip(element);
|
||||
}
|
||||
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
|
||||
Session.set('UpdateCINE', Random.id());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isTouchDevice()) {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'manual'
|
||||
};
|
||||
} else {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'hover'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function resizeViewports() {
|
||||
// Handle resizing of image viewer viewports
|
||||
// For some reason, this seems to need to be on
|
||||
// another delay, or the resizing won't work properly
|
||||
viewportResizeTimer = setTimeout(function() {
|
||||
var elements = $('.imageViewerViewport');
|
||||
elements.each(function(index) {
|
||||
var element = this;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
cornerstone.resize(element, true);
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
|
||||
Session.setDefault('ActiveViewport', 0);
|
||||
Session.setDefault('viewportRows', 1);
|
||||
Session.setDefault('viewportColumns', 1);
|
||||
|
||||
// Avoid doing DOM manipulation during the resize handler
|
||||
// because it is fired very often.
|
||||
// Resizing is therefore performed 100 ms after the resize event stops.
|
||||
var resizeTimer;
|
||||
$(window).on('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
resizeViewports();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
var imageViewer = $("#viewer");
|
||||
if (imageViewer) {
|
||||
$('.navbar-default').css({
|
||||
'background-color': '#000000',
|
||||
'border-color': '#101010'
|
||||
});
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.height = '100%';
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.minWidth = 0;
|
||||
document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
|
||||
}
|
||||
});
|
||||
12
OHIFViewer/client/components/viewer/viewer.styl
Normal file
12
OHIFViewer/client/components/viewer/viewer.styl
Normal file
@ -0,0 +1,12 @@
|
||||
#viewer
|
||||
width: 100%
|
||||
height: calc(100% - 53px)
|
||||
border-top: 1px solid #525252
|
||||
|
||||
.noselect
|
||||
-webkit-touch-callout: none
|
||||
-webkit-user-select: none
|
||||
-khtml-user-select: none
|
||||
-moz-user-select: none
|
||||
-ms-user-select: none
|
||||
user-select: none
|
||||
@ -0,0 +1,6 @@
|
||||
<template name="viewerMain">
|
||||
<div class='viewerMain'>
|
||||
{{ > toolbar }}
|
||||
{{ > imageViewerViewports }}
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,6 @@
|
||||
Template.viewerMain.helpers({
|
||||
studies : function() {
|
||||
var studies = Session.get('studies');
|
||||
return studies;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
.viewerMain
|
||||
float: left
|
||||
background-color: gray
|
||||
height: 100%
|
||||
width: calc(100% - 120px)
|
||||
5
OHIFViewer/client/routeHooks.js
Normal file
5
OHIFViewer/client/routeHooks.js
Normal file
@ -0,0 +1,5 @@
|
||||
Router.onBeforeAction(function () {
|
||||
|
||||
// User is logged in, go ahead and route them
|
||||
this.next();
|
||||
});
|
||||
33
OHIFViewer/client/routes.js
Normal file
33
OHIFViewer/client/routes.js
Normal file
@ -0,0 +1,33 @@
|
||||
Router.configure({
|
||||
layoutTemplate: 'layout',
|
||||
//loadingTemplate: '',
|
||||
notFoundTemplate: 'notFound'
|
||||
});
|
||||
|
||||
|
||||
|
||||
Router.route('/', function () {
|
||||
this.render('worklist', {
|
||||
});
|
||||
});
|
||||
|
||||
ViewerStudies = new Mongo.Collection(null);
|
||||
|
||||
Router.route('/viewer/:_id', {
|
||||
layoutTemplate: 'layout',
|
||||
name: 'viewer',
|
||||
onBeforeAction: function() {
|
||||
var self = this;
|
||||
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
|
||||
//console.log(study);
|
||||
sortStudy(study);
|
||||
|
||||
var studies = [study];
|
||||
Session.set('studies', studies);
|
||||
|
||||
self.render('viewer');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
3045
Packages/cornerstone/client/cornerstone.js
Normal file
3045
Packages/cornerstone/client/cornerstone.js
Normal file
File diff suppressed because it is too large
Load Diff
1779
Packages/cornerstone/client/cornerstoneMath.js
Normal file
1779
Packages/cornerstone/client/cornerstoneMath.js
Normal file
File diff suppressed because it is too large
Load Diff
9913
Packages/cornerstone/client/cornerstoneTools.js
Normal file
9913
Packages/cornerstone/client/cornerstoneTools.js
Normal file
File diff suppressed because it is too large
Load Diff
4202
Packages/cornerstone/client/cornerstoneWADOImageLoader.js
Normal file
4202
Packages/cornerstone/client/cornerstoneWADOImageLoader.js
Normal file
File diff suppressed because it is too large
Load Diff
77
Packages/cornerstone/client/cornerstoneWADORSImageLoader.js
Normal file
77
Packages/cornerstone/client/cornerstoneWADORSImageLoader.js
Normal file
@ -0,0 +1,77 @@
|
||||
cornerstoneWADORSImageLoader = {
|
||||
internal : {
|
||||
nextIndex: 0,
|
||||
imageIds : []
|
||||
}
|
||||
};
|
||||
|
||||
cornerstoneWADORSImageLoader.addImage = function(image) {
|
||||
var index = cornerstoneWADORSImageLoader.internal.nextIndex++;
|
||||
cornerstoneWADORSImageLoader.internal.imageIds[index] = image;
|
||||
var imageId = 'wadors:' + index;
|
||||
return imageId;
|
||||
};
|
||||
|
||||
function getMinMax(storedPixelData)
|
||||
{
|
||||
// we always calculate the min max values since they are not always
|
||||
// present in DICOM and we don't want to trust them anyway as cornerstone
|
||||
// depends on us providing reliable values for these
|
||||
var min = 65535;
|
||||
var max = -32768;
|
||||
var numPixels = storedPixelData.length;
|
||||
var pixelData = storedPixelData;
|
||||
for(var index = 0; index < numPixels; index++) {
|
||||
var spv = pixelData[index];
|
||||
// TODO: test to see if it is faster to use conditional here rather than calling min/max functions
|
||||
min = Math.min(min, spv);
|
||||
max = Math.max(max, spv);
|
||||
}
|
||||
|
||||
return {
|
||||
min: min,
|
||||
max: max
|
||||
};
|
||||
}
|
||||
|
||||
cornerstone.registerImageLoader('wadors', function(imageId) {
|
||||
var index = imageId.substring(7);
|
||||
var image = cornerstoneWADORSImageLoader.internal.imageIds[index];
|
||||
|
||||
var deferred = $.Deferred();
|
||||
|
||||
var mediaType;// = 'image/dicom+jp2';
|
||||
|
||||
DICOMWeb.getImageFrame(image.uri, mediaType).then(function(result) {
|
||||
//console.log(result);
|
||||
// TODO: add support for retrieving compressed pixel data
|
||||
var storedPixelData;
|
||||
if(image.instance.bitsAllocated === 16) {
|
||||
if(image.instance.pixelRepresentation === 0) {
|
||||
storedPixelData = new Uint16Array(result.arrayBuffer, result.offset, result.length / 2);
|
||||
} else {
|
||||
storedPixelData = new Int16Array(result.arrayBuffer, result.offset, result.length / 2);
|
||||
}
|
||||
} else if(image.instance.bitsAllocated === 8) {
|
||||
storedPixelData = new Uint8Array(result.arrayBuffer, result.offset, result.length);
|
||||
}
|
||||
|
||||
// TODO: handle various color space conversions
|
||||
|
||||
var minMax = getMinMax(storedPixelData);
|
||||
image.imageId = imageId;
|
||||
image.minPixelValue = minMax.min;
|
||||
image.maxPixelValue = minMax.max;
|
||||
image.render = cornerstone.renderGrayscaleImage;
|
||||
image.getPixelData = function() {
|
||||
return storedPixelData;
|
||||
};
|
||||
//console.log(image);
|
||||
deferred.resolve(image);
|
||||
}).catch(function(reason) {
|
||||
deferred.reject(reason);
|
||||
});
|
||||
|
||||
return deferred;
|
||||
});
|
||||
|
||||
2212
Packages/cornerstone/client/dicomParser.js
Normal file
2212
Packages/cornerstone/client/dicomParser.js
Normal file
File diff suppressed because it is too large
Load Diff
2495
Packages/cornerstone/client/hammer.js
Normal file
2495
Packages/cornerstone/client/hammer.js
Normal file
File diff suppressed because it is too large
Load Diff
27
Packages/cornerstone/package.js
Normal file
27
Packages/cornerstone/package.js
Normal file
@ -0,0 +1,27 @@
|
||||
Package.describe({
|
||||
name: "cornerstone",
|
||||
summary: "Cornerstone Web-based Medical Imaging libraries",
|
||||
version: '0.0.1'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.versionsFrom('1.2.0.2');
|
||||
|
||||
api.use('jquery');
|
||||
|
||||
api.addFiles('client/cornerstone.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
|
||||
api.addFiles('client/dicomParser.js', 'client', {bare: true});
|
||||
api.addFiles('client/hammer.js', 'client', {bare: true});
|
||||
|
||||
api.export("cornerstone", 'client');
|
||||
api.export("cornerstoneMath", 'client');
|
||||
api.export("cornerstoneTools", 'client');
|
||||
api.export("cornerstoneWADOImageLoader", 'client');
|
||||
api.export("cornerstoneWADORSImageLoader", 'client');
|
||||
api.export("dicomParser", 'client');
|
||||
});
|
||||
|
||||
17
Packages/dicomweb/package.js
Normal file
17
Packages/dicomweb/package.js
Normal file
@ -0,0 +1,17 @@
|
||||
Package.describe({
|
||||
name: "dicomweb",
|
||||
summary: "DICOM Web Helper Functions",
|
||||
version: '0.0.1'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.addFiles('server/namespace.js', 'server');
|
||||
api.addFiles('server/getImageFrame.js', 'server');
|
||||
api.addFiles('server/getJSON.js', 'server');
|
||||
api.addFiles('server/getName.js', 'server');
|
||||
api.addFiles('server/getNumber.js', 'server');
|
||||
api.addFiles('server/getString.js', 'server');
|
||||
|
||||
api.export("DICOMWeb", 'server');
|
||||
});
|
||||
|
||||
70
Packages/dicomweb/server/getImageFrame.js
Normal file
70
Packages/dicomweb/server/getImageFrame.js
Normal file
@ -0,0 +1,70 @@
|
||||
function findBoundary(header) {
|
||||
for(var i=0; i < header.length; i++) {
|
||||
if(header[i].substr(0,2) === '--') {
|
||||
return header[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findContentType(header) {
|
||||
for(var i=0; i < header.length; i++) {
|
||||
if(header[i].substr(0,13) === 'Content-Type:') {
|
||||
return header[i].substr(13).trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
DICOMWeb.getImageFrame = function(uri, mediaType) {
|
||||
mediaType = mediaType || 'application/octet-stream';
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.open("get", uri, true);
|
||||
xhr.setRequestHeader('Accept', 'multipart/related;type=' + mediaType);
|
||||
xhr.onreadystatechange = function (oEvent) {
|
||||
// TODO: consider sending out progress messages here as we receive the pixel data
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
// request succeeded, Parse the multi-part mime response
|
||||
var imageFrameAsArrayBuffer = xhr.response;
|
||||
var response = new Uint8Array(xhr.response);
|
||||
// First look for the multipart mime header
|
||||
var tokenIndex = findIndexOfString(response, '\n\r\n');
|
||||
if(tokenIndex === -1) {
|
||||
reject('invalid response - no multipart mime header');
|
||||
}
|
||||
var header = uint8ArrayToString(response, 0, tokenIndex);
|
||||
// Now find the boundary marker
|
||||
var split = header.split('\r\n');
|
||||
var boundary = findBoundary(split);
|
||||
if(!boundary) {
|
||||
reject('invalid response - no boundary marker');
|
||||
}
|
||||
var offset = tokenIndex + 4; // skip over the \n\r\n
|
||||
|
||||
// find the terminal boundary marker
|
||||
var endIndex = findIndexOfString(response, boundary, offset);
|
||||
if(endIndex === -1) {
|
||||
reject('invalid response - terminating boundary not found');
|
||||
}
|
||||
// return the info for this pixel data
|
||||
var length = endIndex - offset - 1;
|
||||
resolve({
|
||||
contentType: findContentType(split),
|
||||
arrayBuffer: imageFrameAsArrayBuffer,
|
||||
offset: offset,
|
||||
length: length
|
||||
});
|
||||
}
|
||||
else {
|
||||
// request failed, reject the deferred
|
||||
reject(xhr.response);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
};
|
||||
31
Packages/dicomweb/server/getJSON.js
Normal file
31
Packages/dicomweb/server/getJSON.js
Normal file
@ -0,0 +1,31 @@
|
||||
DICOMWeb.getJSON = function(url, options) {
|
||||
var getOptions = {
|
||||
headers: {
|
||||
'Accept' : 'application/json'
|
||||
},
|
||||
};
|
||||
|
||||
if (options.auth) {
|
||||
getOptions.auth = options.auth;
|
||||
}
|
||||
|
||||
if (options.logRequests) {
|
||||
console.log(url);
|
||||
}
|
||||
|
||||
if (options.logTiming) {
|
||||
console.time(url);
|
||||
}
|
||||
|
||||
var result = HTTP.get(url, getOptions);
|
||||
|
||||
if (options.logTiming) {
|
||||
console.timeEnd(url);
|
||||
}
|
||||
|
||||
if (options.logResponses) {
|
||||
console.log(result.data);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
26
Packages/dicomweb/server/getName.js
Normal file
26
Packages/dicomweb/server/getName.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Returns the Alphabetic version of a PN
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The default value to return if the element is not found
|
||||
* @returns {*}
|
||||
*/
|
||||
DICOMWeb.getName = function(element, defaultValue) {
|
||||
if(!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if(!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if(!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Return the Alphabetic component group
|
||||
if(element.Value[0].Alphabetic) {
|
||||
return element.Value[0].Alphabetic;
|
||||
}
|
||||
// Orthanc does not return PN properly so this is a temporary workaround
|
||||
return element.Value[0];
|
||||
};
|
||||
20
Packages/dicomweb/server/getNumber.js
Normal file
20
Packages/dicomweb/server/getNumber.js
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Returns the first string value as a Javascript Number
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The default value to return if the element does not exist
|
||||
* @returns {*}
|
||||
*/
|
||||
DICOMWeb.getNumber = function(element, defaultValue) {
|
||||
if(!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if(!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if(!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
return parseFloat(element.Value[0]);
|
||||
};
|
||||
23
Packages/dicomweb/server/getString.js
Normal file
23
Packages/dicomweb/server/getString.js
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The value to return if the element is not present
|
||||
* @returns {*}
|
||||
*/
|
||||
DICOMWeb.getString = function(element, defaultValue) {
|
||||
if(!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if(!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if(!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Join the array together separated by backslash
|
||||
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
|
||||
return element.Value.join('\\');
|
||||
};
|
||||
1
Packages/dicomweb/server/namespace.js
Normal file
1
Packages/dicomweb/server/namespace.js
Normal file
@ -0,0 +1 @@
|
||||
DICOMWeb = {};
|
||||
31
Packages/viewerbase/components/basic/layout/layout.html
Normal file
31
Packages/viewerbase/components/basic/layout/layout.html
Normal file
@ -0,0 +1,31 @@
|
||||
<template name="layout">
|
||||
<nav class="navbar navbar-default" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">OHIF Viewer</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="http://www.ohif.org">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<form class="navbar-form">
|
||||
<div class="form-group">
|
||||
{{> loginButtons align="right"}}
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{> yield}}
|
||||
</template>
|
||||
18
Packages/viewerbase/components/basic/layout/layout.styl
Normal file
18
Packages/viewerbase/components/basic/layout/layout.styl
Normal file
@ -0,0 +1,18 @@
|
||||
.navbar
|
||||
margin-bottom: 0
|
||||
border-radius: 0px
|
||||
|
||||
.navbar-form
|
||||
margin: 0
|
||||
padding: 15px 3px
|
||||
|
||||
.navbar-default
|
||||
-webkit-transition: background-color 200ms linear
|
||||
-moz-transition: background-color 200ms linear
|
||||
-o-transition: background-color 200ms linear
|
||||
-ms-transition: background-color 200ms linear
|
||||
transition: background-color 200ms linear
|
||||
|
||||
@media (min-width: 768px)
|
||||
.navbar
|
||||
border-radius: 0px
|
||||
3
Packages/viewerbase/components/basic/login/login.html
Normal file
3
Packages/viewerbase/components/basic/login/login.html
Normal file
@ -0,0 +1,3 @@
|
||||
<template name="login">
|
||||
{{> atForm}}
|
||||
</template>
|
||||
@ -0,0 +1,3 @@
|
||||
<template name="notFound">
|
||||
Not found
|
||||
</template>
|
||||
@ -0,0 +1,4 @@
|
||||
<template name="imageThumbnail">
|
||||
<div class="imageThumbnail">
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,11 @@
|
||||
Template.imageThumbnail.onRendered(function() {
|
||||
var instance = this.data.instances[0];
|
||||
var element = this.find('.imageThumbnail');
|
||||
cornerstone.enable(element);
|
||||
|
||||
var imageId = getImageId(instance);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
.imageThumbnail
|
||||
width: 90px
|
||||
height: 90px
|
||||
background-color: black
|
||||
margin: 0 auto
|
||||
@ -0,0 +1,7 @@
|
||||
<template name="studyBrowser">
|
||||
<div class="studyBrowser">
|
||||
{{#each studies}}
|
||||
{{> thumbnails}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,6 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies : function() {
|
||||
var studies = Session.get('studies');
|
||||
return studies;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
.studyBrowser
|
||||
float: left
|
||||
height: 100%
|
||||
width: 120px
|
||||
overflow: auto
|
||||
background-color: black
|
||||
@ -0,0 +1,6 @@
|
||||
<template name="thumbnailEntry">
|
||||
<div class="thumbnailEntry">
|
||||
{{ > imageThumbnail }}
|
||||
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,221 @@
|
||||
function cloneElement(element, targetId) {
|
||||
// Clone the DOM element
|
||||
var clone = element.cloneNode(true);
|
||||
|
||||
// Find any canvas children to clone
|
||||
var clonedCanvases = $(clone).find('canvas');
|
||||
clonedCanvases.each(function(canvasIndex, clonedCanvas) {
|
||||
// Draw from the original canvas to the cloned canvas
|
||||
var context = clonedCanvas.getContext('2d');
|
||||
var thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
|
||||
context.drawImage(thumbnailCanvas, 0, 0);
|
||||
});
|
||||
|
||||
// Update the clone with the targetId
|
||||
clone.id = targetId;
|
||||
clone.style.visibility = 'hidden';
|
||||
return clone;
|
||||
}
|
||||
|
||||
function thumbnailDragStartHandler(e) {
|
||||
// Prevent any scrolling behaviour normally caused by the original event
|
||||
e.originalEvent.preventDefault();
|
||||
|
||||
// Identify the current study and series index from the thumbnail's DOM position
|
||||
var targetThumbnail = e.currentTarget;
|
||||
var imageThumbnail = $(targetThumbnail);
|
||||
|
||||
// Store this data for use during drag and drop
|
||||
OHIF.viewer.dragAndDropData = {
|
||||
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
|
||||
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
|
||||
};
|
||||
|
||||
// Clone the image thumbnail
|
||||
var targetId = targetThumbnail.id + 'DragClone';
|
||||
var clone = cloneElement(targetThumbnail, targetId);
|
||||
clone.classList.add('imageThumbnailClone');
|
||||
|
||||
// Set pointerEvents to pass through the clone DOM element
|
||||
// This is necessary in order to identify what is below it
|
||||
// when using document.elementFromPoint
|
||||
clone.style.pointerEvents = "none";
|
||||
|
||||
// Append the clone to the parent of the target
|
||||
targetThumbnail.parentNode.appendChild(clone);
|
||||
|
||||
// Set the cursor x and y positions from the current touch/mouse coordinates
|
||||
// Handle touchStart cases
|
||||
if (e.type === 'touchstart') {
|
||||
cursorX = e.originalEvent.touches[0].pageX;
|
||||
cursorY = e.originalEvent.touches[0].pageY;
|
||||
} else {
|
||||
cursorX = e.pageX;
|
||||
cursorY = e.pageY;
|
||||
|
||||
// Also hook up event handlers for mouse events
|
||||
$(document).on('mousemove', function(e) {
|
||||
thumbnailDragHandler(e, targetThumbnail);
|
||||
});
|
||||
$(document).on('mouseup', function(e) {
|
||||
thumbnailDragEndHandler(e, targetThumbnail);
|
||||
});
|
||||
}
|
||||
|
||||
// This block gets the current position of the touch/mouse
|
||||
// relative to the thumbnail itself
|
||||
//
|
||||
// i.e. Where did the user grab it from?
|
||||
var position = imageThumbnail.offset();
|
||||
var left = position.left;
|
||||
var top = position.top;
|
||||
|
||||
// This difference is saved for later so the element movement looks normal
|
||||
diffX = cursorX - left;
|
||||
diffY = cursorY - top;
|
||||
|
||||
// This sets the default style properties of the cloned element so it is
|
||||
// ready to be dragged around the page
|
||||
$(clone).css({
|
||||
top: top,
|
||||
left: left,
|
||||
position: 'absolute',
|
||||
'z-index': 100,
|
||||
visibility: 'hidden'
|
||||
});
|
||||
}
|
||||
|
||||
function thumbnailDragHandler(e, target) {
|
||||
// Get the touch/mouse coordinates from the event
|
||||
var cursorX,
|
||||
cursorY;
|
||||
if (e.type === 'touchmove') {
|
||||
cursorX = e.originalEvent.changedTouches[0].pageX;
|
||||
cursorY = e.originalEvent.changedTouches[0].pageY;
|
||||
} else {
|
||||
cursorX = e.pageX;
|
||||
cursorY = e.pageY;
|
||||
}
|
||||
|
||||
// Find the clone element and update it's position on the page
|
||||
var clone = $('#' + target.id + 'DragClone');
|
||||
clone.css({
|
||||
top: cursorY - diffY,
|
||||
left: cursorX - diffX,
|
||||
position: 'absolute',
|
||||
'z-index': 100,
|
||||
visibility: 'visible'
|
||||
});
|
||||
|
||||
// Identify the element below the current cursor position
|
||||
var elemBelow = document.elementFromPoint(cursorX + diffX, cursorY + diffY);
|
||||
|
||||
// If none exists, stop here
|
||||
if (!elemBelow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any current faded effects on viewports
|
||||
$('.imageViewerViewport canvas').removeClass('faded');
|
||||
|
||||
// Figure out what to do depending on what we're dragging over
|
||||
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||
if (viewportsDraggedOver.length) {
|
||||
// If we're dragging over a non-empty viewport, fade it and change the cursor style
|
||||
viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
|
||||
document.body.style.cursor = "copy";
|
||||
} else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
|
||||
// If we're dragging over an empty viewport, just change the cursor style
|
||||
document.body.style.cursor = "copy";
|
||||
} else {
|
||||
// Otherwise, keep the cursor as no-drop style
|
||||
document.body.style.cursor = "no-drop";
|
||||
}
|
||||
}
|
||||
|
||||
function thumbnailDragEndHandler(e, target) {
|
||||
// Remove the mouse event listeners
|
||||
$(document).off('mousemove mouseup');
|
||||
|
||||
// Reset the cursor style to the default
|
||||
document.body.style.cursor = "auto";
|
||||
|
||||
// Get the cloned element
|
||||
var clone = $('#' + target.id + 'DragClone');
|
||||
|
||||
// If it doesn't exist, stop here
|
||||
if (!clone.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var top = clone.offset().top;
|
||||
var left = clone.offset().left;
|
||||
|
||||
// Identify the element below the cloned element position
|
||||
var elemBelow = document.elementFromPoint(left + diffX, top + diffY);
|
||||
|
||||
// Remove all cloned elements from the page
|
||||
$('.imageThumbnailClone').remove();
|
||||
|
||||
// Remove any current faded effects on viewports
|
||||
$('.imageViewerViewport canvas').removeClass('faded');
|
||||
|
||||
// If none exists, stop here
|
||||
if (!elemBelow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any fade effects on the element below
|
||||
elemBelow.classList.remove('faded');
|
||||
|
||||
var element;
|
||||
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||
if (viewportsDraggedOver.length) {
|
||||
// If we're dragging over a non-empty viewport, retrieve it
|
||||
element = viewportsDraggedOver.get(0);
|
||||
} else if (elemBelow.classList.contains('imageViewerViewport') &&
|
||||
elemBelow.classList.contains('empty')) {
|
||||
// If we're dragging over an empty viewport, retrieve that instead
|
||||
element = elemBelow;
|
||||
} else {
|
||||
// Otherwise, stop here
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no store drag and drop data, stop here
|
||||
if (!OHIF.viewer.dragAndDropData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rerender the viewport using the drag and drop data
|
||||
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
|
||||
return false;
|
||||
}
|
||||
|
||||
Template.thumbnailEntry.onRendered(function() {
|
||||
var entry = this.find('.thumbnailEntry');
|
||||
$(entry).data('seriesInstanceUid', Template.parentData(0).seriesInstanceUid);
|
||||
$(entry).data('studyInstanceUid', Template.parentData(1).studyInstanceUid);
|
||||
});
|
||||
|
||||
Template.thumbnailEntry.events({
|
||||
// Touch drag/drop events
|
||||
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
|
||||
thumbnailDragStartHandler(e);
|
||||
},
|
||||
'touchmove .thumbnailEntry': function(e) {
|
||||
thumbnailDragHandler(e, e.currentTarget);
|
||||
},
|
||||
'touchend .thumbnailEntry': function(e){
|
||||
thumbnailDragEndHandler(e, e.currentTarget);
|
||||
}
|
||||
});
|
||||
|
||||
Template.thumbnailEntry.helpers({
|
||||
seriesDescription : function() {
|
||||
if (!this.seriesDescription) {
|
||||
return '';
|
||||
}
|
||||
return this.seriesDescription;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
.thumbnailEntry
|
||||
width: 105px
|
||||
padding: 5px
|
||||
background-color: black
|
||||
border: 1px solid #424242
|
||||
margin: 0 auto
|
||||
|
||||
.seriesDescription
|
||||
text-align: center
|
||||
color: #555
|
||||
word-wrap: break-word
|
||||
|
||||
&:hover
|
||||
background-color: white
|
||||
|
||||
&.active
|
||||
background-color: #424242
|
||||
border-color: #4e4e4e
|
||||
|
||||
.seriesDescription
|
||||
color: white
|
||||
@ -0,0 +1,5 @@
|
||||
<template name="thumbnails">
|
||||
{{#each thumbnails}}
|
||||
{{ >thumbnailEntry }}
|
||||
{{/each}}
|
||||
</template>
|
||||
@ -0,0 +1,6 @@
|
||||
Template.thumbnails.helpers({
|
||||
thumbnails: function() {
|
||||
var stacks = createStacks(this);
|
||||
return stacks;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
<template name="imageViewerViewport">
|
||||
<div class='imageViewerViewport'
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
<div class='viewportInstructions'>
|
||||
Please drag a stack here to view images.
|
||||
</div>
|
||||
{{ >loadingIndicator }}
|
||||
{{ >viewportOverlay }}
|
||||
{{ >viewportOrientationMarkers }}
|
||||
</template>
|
||||
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* This function enables stack prefetching for a specified element (viewport)
|
||||
* It first disables any prefetching currently occurring on any other viewports.
|
||||
* Prefetching will not be enabled if the series is a clip.
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
function enablePrefetchOnElement() {
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
console.log('Enabling prefetch on new element');
|
||||
|
||||
// Loop through all viewports and disable stackPrefetch
|
||||
$('.imageViewerViewport').each(function() {
|
||||
if (!$(this).find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.stackPrefetch.disable(this);
|
||||
});
|
||||
|
||||
// Make sure there is a stack to fetch
|
||||
var stack = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||
cornerstoneTools.stackPrefetch.enable(element);
|
||||
}
|
||||
}
|
||||
|
||||
function displayReferenceLines() {
|
||||
console.log("Changing reference line display");
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
|
||||
$('.imageViewerViewport').each(function(index, element) {
|
||||
if (!$(this).find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === viewportIndex) {
|
||||
cornerstoneTools.referenceLines.tool.disable(element);
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
|
||||
});
|
||||
}
|
||||
|
||||
function loadSeriesIntoViewport(data) {
|
||||
if (!data.series || !data.viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
var study = data.study;
|
||||
var series = data.series;
|
||||
var element = data.viewport;
|
||||
var viewportIndex = $(".imageViewerViewport").index(data.viewport);
|
||||
|
||||
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
|
||||
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
|
||||
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
|
||||
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
|
||||
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
|
||||
|
||||
var imageIds = [];
|
||||
var numImages = series.instances.length;
|
||||
series.instances.forEach(function(instance, imageIndex) {
|
||||
var imageId = getImageId(instance);
|
||||
imageIds.push(imageId);
|
||||
var data = {
|
||||
instance: instance,
|
||||
series: series,
|
||||
study: study,
|
||||
numImages: numImages,
|
||||
imageIndex: imageIndex + 1
|
||||
};
|
||||
addMetaData(imageId, data);
|
||||
});
|
||||
|
||||
var stack = {
|
||||
currentImageIdIndex: 0,
|
||||
imageIds: imageIds
|
||||
};
|
||||
|
||||
var imageId = imageIds[stack.currentImageIdIndex];
|
||||
|
||||
var templateData = Template.currentData();
|
||||
templateData.imageId = imageId;
|
||||
|
||||
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
|
||||
cornerstone.enable(element, cornerstone.webGL.renderer.render);
|
||||
// If you have problems, replace it with this line instead:
|
||||
// cornerstone.enable(element);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
element.classList.remove('empty');
|
||||
$(element).siblings('.viewportInstructions').hide();
|
||||
$(element).siblings('.imageViewerViewportOverlay').show();
|
||||
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
|
||||
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
|
||||
|
||||
// Enable orientation markers, if applicable
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
updateOrientationMarkers(element, viewport);
|
||||
|
||||
// Clear any old stack data
|
||||
cornerstoneTools.clearToolState(element, 'stack');
|
||||
cornerstoneTools.addToolState(element, 'stack', stack);
|
||||
|
||||
// Enable mouse input
|
||||
cornerstoneTools.mouseInput.enable(element);
|
||||
cornerstoneTools.touchInput.enable(element);
|
||||
cornerstoneTools.mouseWheelInput.enable(element);
|
||||
|
||||
var activeTool = toolManager.getActiveTool();
|
||||
toolManager.setActiveTool(activeTool);
|
||||
|
||||
cornerstoneTools.magnify.enable(element);
|
||||
|
||||
function onImageRendered(e, eventData) {
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
}
|
||||
|
||||
$(element).off('CornerstoneImageRendered', onImageRendered);
|
||||
$(element).on('CornerstoneImageRendered', onImageRendered);
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
|
||||
function onNewImage(e, eventData) {
|
||||
// Update the templateData with the new imageId
|
||||
// This allows the template helpers to update reactively
|
||||
templateData.imageId = eventData.enabledElement.image.imageId;
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
}
|
||||
|
||||
$(element).off('CornerstoneNewImage', onNewImage);
|
||||
$(element).on('CornerstoneNewImage', onNewImage);
|
||||
|
||||
function sendActivationTrigger(e, eventData) {
|
||||
var activeViewportIndex = Session.get('ActiveViewport');
|
||||
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
|
||||
if (viewportIndex === activeViewportIndex) {
|
||||
return;
|
||||
}
|
||||
eventData.viewportIndex = viewportIndex;
|
||||
var customEvent = jQuery.Event('ActivateViewport', eventData);
|
||||
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
|
||||
$(e.target).trigger(customEvent, eventData);
|
||||
}
|
||||
|
||||
$(element).off(allEvents, sendActivationTrigger);
|
||||
$(element).on(allEvents, sendActivationTrigger);
|
||||
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
|
||||
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
|
||||
OHIF.viewer.updateImageSynchronizer.add(element);
|
||||
}
|
||||
|
||||
enablePrefetchOnElement();
|
||||
displayReferenceLines();
|
||||
});
|
||||
}
|
||||
|
||||
Template.imageViewerViewport.onRendered(function() {
|
||||
var studies = Session.get('studies');
|
||||
var viewport = this.find(".imageViewerViewport");
|
||||
var viewportIndex = $(".imageViewerViewport").index(viewport);
|
||||
|
||||
this.data.viewportIndex = viewportIndex;
|
||||
|
||||
var data = {
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
if (this.data.seriesInstanceUid !== undefined && this.data.studyInstanceUid !== undefined) {
|
||||
var studyInstanceUid = this.data.studyInstanceUid;
|
||||
var seriesInstanceUid = this.data.seriesInstanceUid;
|
||||
|
||||
studies.every(function(study) {
|
||||
if (study.studyInstanceUid === studyInstanceUid) {
|
||||
data.study = study;
|
||||
study.seriesList.every(function(series) {
|
||||
if (series.seriesInstanceUid === seriesInstanceUid) {
|
||||
data.series = series;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
var stacks = [];
|
||||
studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
var stack = {
|
||||
series: series,
|
||||
study: study
|
||||
};
|
||||
stacks.push(stack);
|
||||
});
|
||||
});
|
||||
|
||||
if (viewportIndex >= stacks.length) {
|
||||
viewport.classList.add('empty');
|
||||
$(viewport).siblings('.viewportInstructions').show();
|
||||
return;
|
||||
}
|
||||
data.series = stacks[viewportIndex].series;
|
||||
data.study = stacks[viewportIndex].study;
|
||||
}
|
||||
loadSeriesIntoViewport(data);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onDestroyed(function() {
|
||||
var element = this.find(".imageViewerViewport");
|
||||
cornerstone.disable(element);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.events({
|
||||
'ActivateViewport .imageViewerViewport': function(e) {
|
||||
console.log('ActivateViewport: ' + e.viewportIndex);
|
||||
Session.set('ActiveViewport', e.viewportIndex);
|
||||
enablePrefetchOnElement();
|
||||
displayReferenceLines();
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
.imageViewerViewport
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: black
|
||||
|
||||
// Prevents the entire page from getting larger
|
||||
// when the magnify tool is near the sides/corners of the page
|
||||
overflow: hidden
|
||||
|
||||
.viewportInstructions
|
||||
display: none
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
line-height: 18px
|
||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||
text-align: center
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
left: 0
|
||||
margin: auto
|
||||
height: 20px
|
||||
@ -0,0 +1,11 @@
|
||||
<template name="imageViewerViewports">
|
||||
<div id='imageViewerViewports'>
|
||||
{{ #each viewportArray }}
|
||||
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
|
||||
<div class="removable">
|
||||
{{ > imageViewerViewport}}
|
||||
</div>
|
||||
</div>
|
||||
{{ /each }}
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,30 @@
|
||||
function getNumViewports() {
|
||||
var viewportRows = Session.get('viewportRows');
|
||||
var viewportColumns = Session.get('viewportColumns');
|
||||
return viewportRows * viewportColumns;
|
||||
}
|
||||
|
||||
Template.imageViewerViewports.helpers({
|
||||
height: function() {
|
||||
var viewportRows = Session.get('viewportRows');
|
||||
return 100 / viewportRows;
|
||||
},
|
||||
width: function() {
|
||||
var viewportColumns = Session.get('viewportColumns');
|
||||
return 100 / viewportColumns;
|
||||
},
|
||||
viewportArray: function() {
|
||||
// This is a really annoying thing to have to do, but Meteor
|
||||
// doesn't want to let me use another type of helper.
|
||||
var numViewports = getNumViewports();
|
||||
|
||||
var array = [];
|
||||
for (var i=0; i < numViewports; ++i) {
|
||||
var data = {
|
||||
viewportIndex: i
|
||||
};
|
||||
array.push(data);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
#imageViewerViewports
|
||||
height: calc(100% - 30px)
|
||||
width: 100%
|
||||
|
||||
.viewportContainer
|
||||
float: left
|
||||
border: 2px solid #777
|
||||
|
||||
.removable
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: relative // Necessary so that the viewportOverlay is on top of the viewports
|
||||
@ -0,0 +1,21 @@
|
||||
<template name="layoutChooser">
|
||||
<div class="layoutChooser pull-right dropdown-menu" role="menu">
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,41 @@
|
||||
function highlightCells(currentCell) {
|
||||
var cells = $('.layoutChooser table td');
|
||||
cells.removeClass('hover');
|
||||
|
||||
currentCell = $(currentCell);
|
||||
var table = currentCell.parents('.layoutChooser table').get(0);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
// Loop through the table row by row
|
||||
// and cell by cell to apply the highlighting
|
||||
for (var i = 0; i < table.rows.length; i++) {
|
||||
row = table.rows[i];
|
||||
if (i <= rowIndex) {
|
||||
for (var j = 0; j < row.cells.length; j++) {
|
||||
if (j <= columnIndex) {
|
||||
cell = row.cells[j];
|
||||
cell.classList.add('hover');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Template.layoutChooser.events({
|
||||
'touchstart .layoutChooser table td, mouseenter .layoutChooser table td': function(evt) {
|
||||
highlightCells(evt.currentTarget);
|
||||
},
|
||||
'click .layoutChooser table td': function(evt) {
|
||||
$('#imageViewerViewports').remove();
|
||||
var container = $(".viewerMain").get(0);
|
||||
UI.render(Template.imageViewerViewports, container);
|
||||
|
||||
var currentCell = $(evt.currentTarget);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
// Add 1 because the indices start from zero
|
||||
Session.set('viewportRows', rowIndex + 1);
|
||||
Session.set('viewportColumns', columnIndex + 1);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
.layoutChooser
|
||||
width: 75px
|
||||
min-width: 75px // to override bootstrap's dropdown-menu class
|
||||
height: 75px
|
||||
|
||||
table
|
||||
margin: 0 auto
|
||||
|
||||
td
|
||||
cursor: pointer
|
||||
border: 2px solid black
|
||||
width: 20px
|
||||
height: 20px
|
||||
transition: background-color 0.1s linear
|
||||
|
||||
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
|
||||
background-color: gray
|
||||
@ -0,0 +1,14 @@
|
||||
<template name="loadingIndicator">
|
||||
<div class="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div class="indicatorContents">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
|
||||
<div class="indicatorContents">
|
||||
<h4>Error Loading Image</h4>
|
||||
<p class='description'>An error has occurred.</p>
|
||||
<p class='details'></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,61 @@
|
||||
Meteor.startup(function() {
|
||||
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
|
||||
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
|
||||
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
|
||||
});
|
||||
|
||||
var loadHandlerTimeout;
|
||||
|
||||
startLoadingHandler = function(element) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
loadHandlerTimeout = setTimeout(function() {
|
||||
var elem = $(element);
|
||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||
elem.find('canvas').not('.magnifyTool').addClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
||||
}, OHIF.viewer.loadIndicatorDelay);
|
||||
};
|
||||
|
||||
doneLoadingHandler = function(element) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
var elem = $(element);
|
||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||
};
|
||||
|
||||
errorLoadingHandler = function(element, imageId, error, source) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
var elem = $(element);
|
||||
|
||||
// Could probably chain all of these, but this is more readable
|
||||
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||
|
||||
// Don't display errors from the stackPrefetch tool
|
||||
if (source === "stackPrefetch") {
|
||||
return;
|
||||
}
|
||||
|
||||
var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
|
||||
errorLoadingIndicator.css('display', 'block');
|
||||
|
||||
var cleanedImageId = imageId;
|
||||
|
||||
// This is just used to expand upon some error messages that are sent
|
||||
// when things fail. An example is a network error throwing the error
|
||||
// which is only described as "network".
|
||||
var errorDetails = {
|
||||
network: "A network error has occurred"
|
||||
// We need to expand this further when we see more obscure error messages
|
||||
};
|
||||
|
||||
if (errorDetails.hasOwnProperty(error)) {
|
||||
error = errorDetails[error];
|
||||
}
|
||||
|
||||
errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
|
||||
if (error) {
|
||||
errorLoadingIndicator.find('.details').text("Details: " + error);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
.imageViewerLoadingIndicator
|
||||
display: none
|
||||
color: #ffa500
|
||||
|
||||
.faded
|
||||
opacity: 0.5
|
||||
|
||||
.imageViewerErrorLoadingIndicator
|
||||
display: none
|
||||
background-color: rgba(0,0,0,0.75)
|
||||
|
||||
p, h4
|
||||
text-align: center
|
||||
color: #ffa500
|
||||
padding: 4px 0px
|
||||
|
||||
p
|
||||
font-size: 11pt
|
||||
|
||||
.loadingIndicator
|
||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||
width: 100%
|
||||
height: 100%
|
||||
font-size: 18px
|
||||
z-index: 100
|
||||
|
||||
.indicatorContents
|
||||
position: absolute
|
||||
top: 40%
|
||||
width: 100%
|
||||
text-align: center
|
||||
@ -0,0 +1,8 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,49 @@
|
||||
updateOrientationMarkers = function(element, viewport) {
|
||||
// Updates the orientation labels on the viewport
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||
|
||||
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
||||
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
||||
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
||||
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
||||
|
||||
var markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
};
|
||||
|
||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||
// the rotation applications
|
||||
if (viewport.vflip) {
|
||||
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
||||
}
|
||||
if (viewport.hflip) {
|
||||
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
||||
}
|
||||
|
||||
// Get the viewport orientation marker DOM elements
|
||||
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
|
||||
var topMarker = viewportOrientationMarkers.find('.topMid');
|
||||
var leftMarker = viewportOrientationMarkers.find('.leftMid');
|
||||
|
||||
// Swap the labels accordingly if the viewport has been rotated
|
||||
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
||||
if (viewport.rotation === 90 || viewport.rotation === -270) {
|
||||
topMarker.text(markers.left);
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
leftMarker.text(markers.top);
|
||||
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
} else {
|
||||
topMarker.text(markers.top);
|
||||
leftMarker.text(markers.left);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
.viewportOrientationMarkers
|
||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||
|
||||
font-size: 15px
|
||||
color: rgb(204, 204, 204)
|
||||
line-height: 18px
|
||||
|
||||
.orientationMarker
|
||||
position: absolute
|
||||
|
||||
.topMid
|
||||
top: 5px
|
||||
left: 50%
|
||||
|
||||
.leftMid
|
||||
top: 47%
|
||||
left: 5px
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
<template name="viewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect">
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{studyDate}} {{studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
<div>{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>Ser: {{seriesNumber}}</div>
|
||||
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||
<div>{{frameRate}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,155 @@
|
||||
function getElementIfNotEmpty(viewportIndex) {
|
||||
// Meteor template helpers run more often than expected
|
||||
// They often seem to run just before the whole template is rendered
|
||||
// This meant that the onRendered event hadn't fired yet, so the
|
||||
// element wasn't enabled / set empty yet. The check here
|
||||
// for canvases under the 'enabled' element div is to prevent
|
||||
// 'undefined' errors from the helper functions
|
||||
|
||||
var imageViewerViewports = $('.imageViewerViewport'),
|
||||
element = imageViewerViewports.get(viewportIndex),
|
||||
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
|
||||
|
||||
if (!element || $(element).hasClass("empty") || canvases.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to make sure the element is enabled.
|
||||
try {
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function getPatient(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||
if (!patient) {
|
||||
return '';
|
||||
}
|
||||
return patient[property];
|
||||
}
|
||||
|
||||
function getStudy(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||
if (!study) {
|
||||
return '';
|
||||
}
|
||||
return study[property];
|
||||
}
|
||||
|
||||
function getSeries(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||
if (!series) {
|
||||
return '';
|
||||
}
|
||||
return series[property];
|
||||
}
|
||||
|
||||
function getInstance(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||
if (!instance) {
|
||||
return '';
|
||||
}
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
function getImage(viewportIndex) {
|
||||
var element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return false;
|
||||
}
|
||||
return enabledElement.image;
|
||||
}
|
||||
|
||||
Template.viewportOverlay.helpers({
|
||||
wwwc: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
||||
},
|
||||
zoom: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return (viewport.scale * 100.0);
|
||||
},
|
||||
imageDimensions: function() {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
|
||||
var image = getImage(this.viewportIndex);
|
||||
if (!image) {
|
||||
return '';
|
||||
}
|
||||
return image.width + ' x ' + image.height;
|
||||
},
|
||||
patientName : function() {
|
||||
return getPatient.call(this, 'name');
|
||||
},
|
||||
patientId : function() {
|
||||
return getPatient.call(this, 'id');
|
||||
},
|
||||
studyDate : function() {
|
||||
return getStudy.call(this, 'date');
|
||||
},
|
||||
studyTime : function() {
|
||||
return getStudy.call(this, 'time');
|
||||
},
|
||||
studyDescription : function() {
|
||||
return getStudy.call(this, 'description');
|
||||
},
|
||||
seriesDescription : function() {
|
||||
return getSeries.call(this, 'description');
|
||||
},
|
||||
seriesNumber : function() {
|
||||
return getSeries.call(this, 'number');
|
||||
},
|
||||
imageNumber : function() {
|
||||
return getInstance.call(this, 'number');
|
||||
},
|
||||
imageIndex : function() {
|
||||
return getInstance.call(this, 'index');
|
||||
},
|
||||
numImages : function() {
|
||||
return getSeries.call(this, 'numImages');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
.imageViewerViewportOverlay
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
line-height: 18px
|
||||
display: none // Shown when an image is loaded
|
||||
|
||||
.dicomTag
|
||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||
position: absolute
|
||||
|
||||
.topleft
|
||||
top: 3px
|
||||
left: 3px
|
||||
|
||||
.topright
|
||||
top: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
|
||||
.bottomleft
|
||||
bottom: 3px
|
||||
left: 3px
|
||||
|
||||
.bottomright
|
||||
bottom: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user