Initial work on Nucleus parity merge
This commit is contained in:
parent
aa88fe181a
commit
680beb163f
@ -12,7 +12,8 @@ const OHIF = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Expose the OHIF object to the client if it is on development mode
|
// Expose the OHIF object to the client if it is on development mode
|
||||||
if (Meteor.isDevelopment && Meteor.isClient) {
|
// @TODO: remove this after applying namespace to this package
|
||||||
|
if (Meteor.isClient) {
|
||||||
window.OHIF = OHIF;
|
window.OHIF = OHIF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
<template name="textMarkerDialogs">
|
||||||
|
<dialog id="textMarkerOptionsDialog" oncontextmenu="return false" class="textMarkerDialog noselect">
|
||||||
|
<h5>Spine Labels</h5>
|
||||||
|
<a class="closeTextMarkerDialogs">{{> iconCrossCircle}}</a>
|
||||||
|
<div class='optionsDiv'>
|
||||||
|
<div class="select2-wrapper optionBox">
|
||||||
|
<label for="startFrom">Label: </label>
|
||||||
|
<select name="start" id="startFrom" tabindex="-1" class="select2">
|
||||||
|
<!-- Cervical spine !-->
|
||||||
|
<option value="C1" selected>C1</option>
|
||||||
|
<option value="C2">C2</option>
|
||||||
|
<option value="C3">C3</option>
|
||||||
|
<option value="C4">C4</option>
|
||||||
|
<option value="C5">C5</option>
|
||||||
|
<option value="C6">C6</option>
|
||||||
|
<option value="C7">C7</option>
|
||||||
|
|
||||||
|
<!-- Thoracic spine !-->
|
||||||
|
<option value="T1">T1</option>
|
||||||
|
<option value="T2">T2</option>
|
||||||
|
<option value="T3">T3</option>
|
||||||
|
<option value="T4">T4</option>
|
||||||
|
<option value="T5">T5</option>
|
||||||
|
<option value="T6">T6</option>
|
||||||
|
<option value="T7">T7</option>
|
||||||
|
<option value="T8">T8</option>
|
||||||
|
<option value="T9">T9</option>
|
||||||
|
<option value="T10">T10</option>
|
||||||
|
<option value="T11">T11</option>
|
||||||
|
<option value="T12">T12</option>
|
||||||
|
|
||||||
|
<!-- Lumbar spine !-->
|
||||||
|
<option value="L1">L1</option>
|
||||||
|
<option value="L2">L2</option>
|
||||||
|
<option value="L3">L3</option>
|
||||||
|
<option value="L4">L4</option>
|
||||||
|
<option value="L5">L5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="optionBox">
|
||||||
|
<div class="btn-group iconSwitch" data-toggle="buttons">
|
||||||
|
<label for="ascending" class="btn btn-link" title="Add labels from head to feet (down) or feet to head (up)" data-toggle="tooltip" data-container="body">
|
||||||
|
<input type="checkbox" id="ascending"> <span class="on"><i class="fa fa-long-arrow-up" aria-hidden="true"></i></span> <span class="off"><i class="fa fa-long-arrow-down" aria-hidden="true"></i></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="clearLabels" class="btn viewerBtn btn-xs" type="button">
|
||||||
|
Clear Labels <i class="viewerIcon-clear"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="textMarkerRelabelDialog" oncontextmenu="return false" class="textMarkerDialog noselect">
|
||||||
|
<h5>Change Spine Label</h5>
|
||||||
|
<div class="relabelOptions">
|
||||||
|
|
||||||
|
<div class="select2-wrapper optionBox">
|
||||||
|
<label for="relabelSelect">Label:</label>
|
||||||
|
<select name="relabelSelect" class="relabelSelect">
|
||||||
|
<!-- Cervical spine !-->
|
||||||
|
<option value="C1">C1</option>
|
||||||
|
<option value="C2">C2</option>
|
||||||
|
<option value="C3">C3</option>
|
||||||
|
<option value="C4">C4</option>
|
||||||
|
<option value="C5">C5</option>
|
||||||
|
<option value="C6">C6</option>
|
||||||
|
<option value="C7">C7</option>
|
||||||
|
|
||||||
|
<!-- Thoracic spine !-->
|
||||||
|
<option value="T1">T1</option>
|
||||||
|
<option value="T2">T2</option>
|
||||||
|
<option value="T3">T3</option>
|
||||||
|
<option value="T4">T4</option>
|
||||||
|
<option value="T5">T5</option>
|
||||||
|
<option value="T6">T6</option>
|
||||||
|
<option value="T7">T7</option>
|
||||||
|
<option value="T8">T8</option>
|
||||||
|
<option value="T9">T9</option>
|
||||||
|
<option value="T10">T10</option>
|
||||||
|
<option value="T11">T11</option>
|
||||||
|
<option value="T12">T12</option>
|
||||||
|
|
||||||
|
<!-- Lumbar spine !-->
|
||||||
|
<option value="L1">L1</option>
|
||||||
|
<option value="L2">L2</option>
|
||||||
|
<option value="L3">L3</option>
|
||||||
|
<option value="L4">L4</option>
|
||||||
|
<option value="L5">L5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relabelButtons">
|
||||||
|
<button class="relabelRemove btn btn-xs btn-secondary viewerBtn">Delete</button>
|
||||||
|
<button class="relabelConfirm btn btn-xs viewerBtn">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="dialog arrow">
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* @TODO: add this to OHIF's Viewers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Template } from 'meteor/templating';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
|
||||||
|
import { toolManager } from '../../../lib/toolManager';
|
||||||
|
import { viewportUtils } from '../../../lib/viewportUtils';
|
||||||
|
|
||||||
|
Template.textMarkerDialogs.events({
|
||||||
|
'change #startFrom'(e) {
|
||||||
|
const config = cornerstoneTools.textMarker.getConfiguration();
|
||||||
|
config.current = $(e.target).val();
|
||||||
|
//console.log("Changed starting point to: " + config.current);
|
||||||
|
},
|
||||||
|
'change #ascending'(e) {
|
||||||
|
const config = cornerstoneTools.textMarker.getConfiguration();
|
||||||
|
config.ascending = $(e.target).is(':checked');
|
||||||
|
|
||||||
|
const currentIndex = config.markers.indexOf(config.current);
|
||||||
|
|
||||||
|
config.current = config.markers[currentIndex];
|
||||||
|
const nextMarker = config.current;
|
||||||
|
$('#startFrom').val(nextMarker).trigger('change');
|
||||||
|
//console.log("Changed ascending to: " + config.ascending);
|
||||||
|
},
|
||||||
|
'click #clearLabels'() {
|
||||||
|
const element = viewportUtils.getActiveViewportElement();
|
||||||
|
const toolType = 'textMarker';
|
||||||
|
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
const toolState = toolStateManager.toolState;
|
||||||
|
|
||||||
|
// We might want to make this a convenience function in cornerstoneTools
|
||||||
|
const stack = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
if (stack && stack.data.length && stack.data[0].imageIds.length) {
|
||||||
|
const imageIds = stack.data[0].imageIds;
|
||||||
|
|
||||||
|
// Clear the tool data for each image in the stack
|
||||||
|
imageIds.forEach( imageId => {
|
||||||
|
if(toolState.hasOwnProperty(imageId)) {
|
||||||
|
const toolData = toolState[imageId];
|
||||||
|
if (toolData.hasOwnProperty(toolType)) {
|
||||||
|
delete toolData[toolType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
},
|
||||||
|
'click .closeTextMarkerDialogs'() {
|
||||||
|
const defaultTool = toolManager.getDefaultTool();
|
||||||
|
toolManager.setActiveTool(defaultTool);
|
||||||
|
document.getElementById('textMarkerOptionsDialog').close();
|
||||||
|
$('#spine').removeClass('active');
|
||||||
|
$('#' + defaultTool).addClass('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.textMarkerDialogs.onRendered(function() {
|
||||||
|
const optionsDialog = $('#textMarkerOptionsDialog');
|
||||||
|
optionsDialog.draggable();
|
||||||
|
dialogPolyfill.registerDialog(optionsDialog.get(0));
|
||||||
|
|
||||||
|
const relabelDialog = $('#textMarkerRelabelDialog');
|
||||||
|
relabelDialog.draggable();
|
||||||
|
dialogPolyfill.registerDialog(relabelDialog.get(0));
|
||||||
|
|
||||||
|
$(document).on('click', event => {
|
||||||
|
if (!$(event.target).closest('.select2-wrapper').length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#startFrom, .relabelSelect').select2('close');
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('touchmove', event => {
|
||||||
|
if (!$(event.target).closest('.select2-container').length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#startFrom, .relabelSelect').select2('close');
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
|
||||||
|
FastClick.attach(document.body);
|
||||||
|
|
||||||
|
const $customSelects = $('#startFrom, .relabelSelect')
|
||||||
|
|
||||||
|
$customSelects.select2({
|
||||||
|
/**
|
||||||
|
* Adds needsclick class to all DOM elements in the Select2 results list
|
||||||
|
* so they can be accessible on iOS mobile when FastClick is initiated too.
|
||||||
|
*/
|
||||||
|
templateResult(result, container) {
|
||||||
|
if (!result.id) {
|
||||||
|
return result.text;
|
||||||
|
}
|
||||||
|
container.className += ' needsclick';
|
||||||
|
return result.text;
|
||||||
|
},
|
||||||
|
placeholder: 'C1',
|
||||||
|
minimumResultsForSearch: -1,
|
||||||
|
theme: 'viewerDropdown'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional to tweaking the templateResult option in Select2,
|
||||||
|
* add needsclick class to all DOM elements in the Select2 container,
|
||||||
|
* so they can be accessible on iOS mobile when FastClick is initiated too.
|
||||||
|
*
|
||||||
|
* More info about needsclick:
|
||||||
|
* https://github.com/ftlabs/fastclick#ignore-certain-elements-with-needsclick
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
$customSelects.each( (index, el) =>{
|
||||||
|
$(el).data('select2').$container.find('*').addClass('needsclick');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
.textMarkerDialog
|
||||||
|
z-index: 1000
|
||||||
|
width: 260px
|
||||||
|
position: absolute
|
||||||
|
top: auto
|
||||||
|
left: auto
|
||||||
|
bottom: 3px
|
||||||
|
right: 3px
|
||||||
|
margin: auto
|
||||||
|
padding:.5em
|
||||||
|
background-image: linear-gradient(#2E608D, #23557F)
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 1px black solid
|
||||||
|
border-radius: 5px
|
||||||
|
height: 81px
|
||||||
|
max-height: 81px
|
||||||
|
h5
|
||||||
|
color: #D1EDFB;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor:default
|
||||||
|
.handle
|
||||||
|
width: 40px;
|
||||||
|
float: left;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-left: 1px;
|
||||||
|
#startFrom
|
||||||
|
margin-right:10px
|
||||||
|
#clearLabels
|
||||||
|
margin-left:10px
|
||||||
|
float:right
|
||||||
|
.optionBox
|
||||||
|
color: #d1edfb
|
||||||
|
margin-bottom: 5px
|
||||||
|
float: left
|
||||||
|
span.subLabel
|
||||||
|
width: 50px
|
||||||
|
display: block
|
||||||
|
float: left
|
||||||
|
line-height: 30px
|
||||||
|
> label
|
||||||
|
color: #d1edfb
|
||||||
|
float: left
|
||||||
|
min-width: 40px
|
||||||
|
line-height: 24px
|
||||||
|
font-size: 12px
|
||||||
|
font-weight: 600
|
||||||
|
.closeTextMarkerDialogs
|
||||||
|
position: absolute
|
||||||
|
top: 5px
|
||||||
|
right: 4px
|
||||||
|
padding: 5px
|
||||||
|
cursor: pointer
|
||||||
|
svg
|
||||||
|
width: 19px
|
||||||
|
height: 19px
|
||||||
|
fill: #d1edfb
|
||||||
|
&:hover
|
||||||
|
svg
|
||||||
|
fill: #FFF
|
||||||
|
.dialog.arrow
|
||||||
|
left: 50%
|
||||||
|
margin-left: -11px
|
||||||
|
border-top-width: 0
|
||||||
|
border-bottom-color: #999999
|
||||||
|
border-bottom-color: rgba(0, 0, 0, 0.25)
|
||||||
|
border-width: 11px
|
||||||
|
bottom: -10px
|
||||||
|
position: absolute
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid #23557f;
|
||||||
|
&:after
|
||||||
|
content: " "
|
||||||
|
top: 1px
|
||||||
|
margin-left: -10px
|
||||||
|
border-top-width: 0
|
||||||
|
border-bottom-color: #23557f
|
||||||
|
border-width: 10px
|
||||||
|
|
||||||
|
#textMarkerRelabelDialog
|
||||||
|
margin: 0
|
||||||
|
.relabelOptions
|
||||||
|
padding: 15px 0px 0px 0px
|
||||||
|
.relabelSelect
|
||||||
|
margin-left: 5px
|
||||||
|
|
||||||
|
#textMarkerOptionsDialog
|
||||||
|
.optionsDiv
|
||||||
|
padding-top: 10px
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.relabelButtons
|
||||||
|
text-align:right
|
||||||
|
|
||||||
|
button.viewerBtn
|
||||||
|
background-color: #5D9ACE;
|
||||||
|
border: 1px solid #6EAEE4;
|
||||||
|
font-size: 12px !important;
|
||||||
|
color:#FFF
|
||||||
|
line-height: 21px;
|
||||||
|
&:hover
|
||||||
|
color:#FFF
|
||||||
|
|
||||||
|
button.btn-secondary
|
||||||
|
background-color: #CFE3F5;
|
||||||
|
border: 1px solid #E5F3FF;
|
||||||
|
font-size: 12px !important;
|
||||||
|
color:#0D416D
|
||||||
|
&:hover
|
||||||
|
color:#0D416D
|
||||||
|
|
||||||
|
.iconSwitch
|
||||||
|
margin:0px 5px
|
||||||
|
.btn
|
||||||
|
span
|
||||||
|
display: none
|
||||||
|
.on
|
||||||
|
display: none
|
||||||
|
.off
|
||||||
|
display: block
|
||||||
|
&.btn-link, &.active, &:active, &:hover, &:focus, &.active:focus
|
||||||
|
background-color: #225682;
|
||||||
|
border: 1px solid #7AB0DE;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d1edfb;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
&:hover
|
||||||
|
color:#fff
|
||||||
|
&.active
|
||||||
|
.on
|
||||||
|
display:block
|
||||||
|
.off
|
||||||
|
display: none
|
||||||
|
|
||||||
|
|
||||||
191
Packages/ohif-viewerbase/client/index.js
Normal file
191
Packages/ohif-viewerbase/client/index.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { Viewerbase } from '../namespace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// getElementIfNotEmpty
|
||||||
|
import { getElementIfNotEmpty } from './lib/getElementIfNotEmpty';
|
||||||
|
Viewerbase.getElementIfNotEmpty = getElementIfNotEmpty;
|
||||||
|
|
||||||
|
// getStackDataIfNotEmpty
|
||||||
|
import { getStackDataIfNotEmpty } from './lib/getStackDataIfNotEmpty';
|
||||||
|
Viewerbase.getStackDataIfNotEmpty = getStackDataIfNotEmpty;
|
||||||
|
|
||||||
|
// switchToImageRelative
|
||||||
|
import { switchToImageRelative } from './lib/switchToImageRelative';
|
||||||
|
Viewerbase.switchToImageRelative = switchToImageRelative;
|
||||||
|
|
||||||
|
// switchToImageByIndex
|
||||||
|
import { switchToImageByIndex } from './lib/switchToImageByIndex';
|
||||||
|
Viewerbase.switchToImageByIndex = switchToImageByIndex;
|
||||||
|
|
||||||
|
// getFrameOfReferenceUID
|
||||||
|
import { getFrameOfReferenceUID } from './lib/getFrameOfReferenceUID';
|
||||||
|
Viewerbase.getFrameOfReferenceUID = getFrameOfReferenceUID;
|
||||||
|
|
||||||
|
// updateCrosshairsSynchronizer
|
||||||
|
import { updateCrosshairsSynchronizer } from './lib/updateCrosshairsSynchronizer';
|
||||||
|
Viewerbase.updateCrosshairsSynchronizer = updateCrosshairsSynchronizer;
|
||||||
|
|
||||||
|
// getImageId
|
||||||
|
import { getImageId } from './lib/getImageId';
|
||||||
|
Viewerbase.getImageId = getImageId;
|
||||||
|
|
||||||
|
// setActiveViewport
|
||||||
|
import { setActiveViewport } from './lib/setActiveViewport';
|
||||||
|
Viewerbase.setActiveViewport = setActiveViewport;
|
||||||
|
|
||||||
|
// classifyImageOrientation
|
||||||
|
import { classifyImageOrientation } from './lib/classifyImageOrientation';
|
||||||
|
Viewerbase.classifyImageOrientation = classifyImageOrientation;
|
||||||
|
|
||||||
|
// setFocusToActiveViewport
|
||||||
|
import { setFocusToActiveViewport } from './lib/setFocusToActiveViewport';
|
||||||
|
Viewerbase.setFocusToActiveViewport = setFocusToActiveViewport;
|
||||||
|
|
||||||
|
// getWADORSImageId
|
||||||
|
import { getWADORSImageId } from './lib/getWADORSImageId';
|
||||||
|
Viewerbase.getWADORSImageId = getWADORSImageId;
|
||||||
|
|
||||||
|
// updateAllViewports
|
||||||
|
import { updateAllViewports } from './lib/updateAllViewports';
|
||||||
|
Viewerbase.updateAllViewports = updateAllViewports;
|
||||||
|
|
||||||
|
// sortStudy
|
||||||
|
import { sortStudy } from './lib/sortStudy';
|
||||||
|
Viewerbase.sortStudy = sortStudy;
|
||||||
|
|
||||||
|
// updateOrientationMarkers
|
||||||
|
import { updateOrientationMarkers } from './lib/updateOrientationMarkers';
|
||||||
|
Viewerbase.updateOrientationMarkers = updateOrientationMarkers;
|
||||||
|
|
||||||
|
// isImage
|
||||||
|
import { isImage } from './lib/isImage';
|
||||||
|
Viewerbase.isImage = isImage;
|
||||||
|
|
||||||
|
// getInstanceClassDefaultViewport, setInstanceClassDefaultViewportFunction
|
||||||
|
import { getInstanceClassDefaultViewport, setInstanceClassDefaultViewportFunction } from './lib/instanceClassSpecificViewport';
|
||||||
|
Viewerbase.getInstanceClassDefaultViewport = getInstanceClassDefaultViewport;
|
||||||
|
Viewerbase.setInstanceClassDefaultViewportFunction = setInstanceClassDefaultViewportFunction;
|
||||||
|
|
||||||
|
// setMammogramViewportAlignment
|
||||||
|
import { setMammogramViewportAlignment } from './lib/setMammogramViewportAlignment';
|
||||||
|
Viewerbase.setMammogramViewportAlignment = setMammogramViewportAlignment;
|
||||||
|
|
||||||
|
// addMetaData, addSpecificMetadata, updateMetaData
|
||||||
|
import { addMetaData, addSpecificMetadata, updateMetaData } from './lib/metaDataProvider';
|
||||||
|
Viewerbase.addMetaData = addMetaData;
|
||||||
|
Viewerbase.addSpecificMetadata = addSpecificMetadata;
|
||||||
|
Viewerbase.updateMetaData = updateMetaData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Namespaces (sub-namespaces)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// imageViewerViewportData.*
|
||||||
|
import { imageViewerViewportData } from './lib/imageViewerViewportData';
|
||||||
|
Viewerbase.imageViewerViewportData = imageViewerViewportData;
|
||||||
|
|
||||||
|
// panelNavigation.*
|
||||||
|
import { panelNavigation } from './lib/panelNavigation';
|
||||||
|
Viewerbase.panelNavigation = panelNavigation;
|
||||||
|
|
||||||
|
// seriesNavigation.*
|
||||||
|
import { seriesNavigation } from './lib/seriesNavigation';
|
||||||
|
Viewerbase.seriesNavigation = seriesNavigation;
|
||||||
|
|
||||||
|
// WLPresets.*
|
||||||
|
import { WLPresets } from './lib/WLPresets';
|
||||||
|
Viewerbase.wlPresets = WLPresets;
|
||||||
|
|
||||||
|
// hotkeyUtils.*
|
||||||
|
import { hotkeyUtils } from './lib/hotkeyUtils';
|
||||||
|
Viewerbase.hotkeyUtils = hotkeyUtils;
|
||||||
|
|
||||||
|
// viewportOverlayUtils.*
|
||||||
|
import { viewportOverlayUtils } from './lib/viewportOverlayUtils';
|
||||||
|
Viewerbase.viewportOverlayUtils = viewportOverlayUtils;
|
||||||
|
|
||||||
|
// viewportUtils.*
|
||||||
|
import { viewportUtils } from './lib/viewportUtils';
|
||||||
|
Viewerbase.viewportUtils = viewportUtils;
|
||||||
|
|
||||||
|
// thumbnailDragHandlers.*
|
||||||
|
import { thumbnailDragHandlers } from './lib/thumbnailDragHandlers';
|
||||||
|
Viewerbase.thumbnailDragHandlers = thumbnailDragHandlers;
|
||||||
|
|
||||||
|
// dialogUtils.*
|
||||||
|
import { dialogUtils } from './lib/dialogUtils';
|
||||||
|
Viewerbase.dialogUtils = dialogUtils;
|
||||||
|
|
||||||
|
// unloadHandlers.*
|
||||||
|
import { unloadHandlers } from './lib/unloadHandlers';
|
||||||
|
Viewerbase.unloadHandlers = unloadHandlers;
|
||||||
|
|
||||||
|
// sortingManager.*
|
||||||
|
import { sortingManager } from './lib/sortingManager';
|
||||||
|
Viewerbase.sortingManager = sortingManager;
|
||||||
|
|
||||||
|
// crosshairsSynchronizers.*
|
||||||
|
import { crosshairsSynchronizers } from './lib/crosshairsSynchronizers';
|
||||||
|
Viewerbase.crosshairsSynchronizers = crosshairsSynchronizers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Singletons
|
||||||
|
*/
|
||||||
|
|
||||||
|
// StackManager as "stackManager" (since it's a plain object instance, the exported name starts with a lowercase letter)
|
||||||
|
import { StackManager } from './lib/StackManager';
|
||||||
|
Viewerbase.stackManager = StackManager;
|
||||||
|
|
||||||
|
// toolManager
|
||||||
|
import { toolManager } from './lib/toolManager';
|
||||||
|
Viewerbase.toolManager = toolManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { helpers } from './lib/helpers/';
|
||||||
|
Viewerbase.helpers = helpers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Collections
|
||||||
|
*/
|
||||||
|
|
||||||
|
// sopClassDictionary
|
||||||
|
import { sopClassDictionary } from './lib/sopClassDictionary';
|
||||||
|
Viewerbase.sopClassDictionary = sopClassDictionary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ImageSet
|
||||||
|
import { ImageSet } from './lib/classes/ImageSet';
|
||||||
|
Viewerbase.ImageSet = ImageSet;
|
||||||
|
|
||||||
|
// ResizeViewportManager
|
||||||
|
import { ResizeViewportManager } from './lib/classes/ResizeViewportManager';
|
||||||
|
Viewerbase.ResizeViewportManager = ResizeViewportManager;
|
||||||
|
|
||||||
|
// StudyMetadata, SeriesMetadata, InstanceMetadata
|
||||||
|
import { StudyMetadata } from './lib/classes/metadata/StudyMetadata';
|
||||||
|
import { SeriesMetadata } from './lib/classes/metadata/SeriesMetadata';
|
||||||
|
import { InstanceMetadata } from './lib/classes/metadata/InstanceMetadata';
|
||||||
|
Viewerbase.metadata = { StudyMetadata, SeriesMetadata, InstanceMetadata };
|
||||||
|
|
||||||
|
// TypeSafeCollection
|
||||||
|
import { TypeSafeCollection } from './lib/classes/TypeSafeCollection';
|
||||||
|
Viewerbase.TypeSafeCollection = TypeSafeCollection;
|
||||||
|
|
||||||
|
// OHIFError
|
||||||
|
import { OHIFError } from './lib/classes/OHIFError';
|
||||||
|
Viewerbase.OHIFError = OHIFError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports for Side Effects Only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './lib/debugReactivity';
|
||||||
64
Packages/ohif-viewerbase/client/lib/MetadataViews.js
Normal file
64
Packages/ohif-viewerbase/client/lib/MetadataViews.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Default Configuration to handle Metadata from WADO or DIMSE,
|
||||||
|
// currently retrieved through Meteor server
|
||||||
|
let configuration = {
|
||||||
|
// These are examples, they need to actually check the properties of the Objects
|
||||||
|
getSeries: (study, seriesNumOrUid) => {
|
||||||
|
// Use Array to find the appropriate Series
|
||||||
|
return study.seriesList.find(series => series['seriesNumber'] === value);
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstance: (series, instanceNumOrUid) => {
|
||||||
|
return series.instances.find(instance => instance['instanceNumber'] === instanceNumOrUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceTagRaw: (instance, tag) => {
|
||||||
|
// (this won't actually work yet)
|
||||||
|
return instance[tag];
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceTagNum: (instance, tag) => {
|
||||||
|
// (this won't actually work yet)
|
||||||
|
return parseFloat(instance[tag]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example Nucleus configuration
|
||||||
|
//
|
||||||
|
configuration = {
|
||||||
|
getSeries: (study, seriesNumOrUid) => study.studyView.series(seriesNumOrInstanceUid),
|
||||||
|
getInstance: (series, instanceNumOrUid) => series.seriesView.instance(instanceNumOrUid),
|
||||||
|
getInstanceTagRaw: (instance, tag) => instance.instanceView.raw(tag),
|
||||||
|
getInstanceTagNum: (instance, tag) => instance.instanceView.num(tag),
|
||||||
|
getCustom(instance, custom) => instance.instanceView[custom]
|
||||||
|
}
|
||||||
|
|
||||||
|
class StudyMetadataView {
|
||||||
|
series(tag, value) {
|
||||||
|
// Retrieves Series by Series Number or SeriesInstanceUid
|
||||||
|
return configuration.getSeries(study, tag, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeriesMetadataView {
|
||||||
|
instances(tag, value) {
|
||||||
|
// Retrieves Instances by Instances Number or SOPInstanceUid
|
||||||
|
return configuration.getInstance(series, tag, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstanceMetadataView {
|
||||||
|
/* Retrieves raw value of tag from instance */
|
||||||
|
raw(tag) {
|
||||||
|
return configuration.getInstanceTagRaw(instance, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieves raw value of tag from instance */
|
||||||
|
num(tag) {
|
||||||
|
return configuration.getInstanceTagNum(instance, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
custom(custom) {
|
||||||
|
return configuration.getCustom(instance, custom)
|
||||||
|
}
|
||||||
|
// etc.. Date, DateTime, ...? Sequence...
|
||||||
|
}
|
||||||
124
Packages/ohif-viewerbase/client/lib/StackManager.js
Normal file
124
Packages/ohif-viewerbase/client/lib/StackManager.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
import { getImageId } from './getImageId';
|
||||||
|
import { addMetaData } from './metaDataProvider';
|
||||||
|
import { OHIFError } from './classes/OHIFError';
|
||||||
|
|
||||||
|
let stackMap = {};
|
||||||
|
let configuration = {};
|
||||||
|
const stackUpdatedCallbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop through the current series and add metadata to the
|
||||||
|
* Cornerstone meta data provider. This will be used to fill information
|
||||||
|
* into the viewport overlays, and to calculate reference lines and orientation markers
|
||||||
|
* @param {Object} stackMap stackMap object
|
||||||
|
* @param {Object} study Study object
|
||||||
|
* @param {Object} displaySet The set of images to make the stack from
|
||||||
|
* @return {Array} Array with image IDs
|
||||||
|
*/
|
||||||
|
function createAndAddStack(stackMap, study, displaySet) {
|
||||||
|
const numImages = displaySet.images.length;
|
||||||
|
const imageIds = [];
|
||||||
|
let imageId;
|
||||||
|
|
||||||
|
displaySet.images.forEach((image, imageIndex) => {
|
||||||
|
const metaData = {
|
||||||
|
instance: image,
|
||||||
|
series: displaySet, // TODO: Check this
|
||||||
|
study: study,
|
||||||
|
numImages: numImages,
|
||||||
|
imageIndex: imageIndex + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const numFrames = image.numFrames;
|
||||||
|
if (numFrames > 1) {
|
||||||
|
OHIF.log.info('Multiframe image detected');
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
metaData.frame = i;
|
||||||
|
imageId = getImageId(image, i);
|
||||||
|
imageIds.push(imageId);
|
||||||
|
addMetaData(imageId, metaData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
imageId = getImageId(image);
|
||||||
|
imageIds.push(imageId);
|
||||||
|
addMetaData(imageId, metaData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return imageIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
createAndAddStack: createAndAddStack
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object contains all the functions needed for interacting with the stack manager.
|
||||||
|
* Generally, findStack is the only function used. If you want to know when new stacks
|
||||||
|
* come in, you can register a callback with addStackUpdatedCallback.
|
||||||
|
* clearStacks and makeAndAddStack should not used outside of loadStudy and removeStudies.
|
||||||
|
*/
|
||||||
|
const StackManager = {
|
||||||
|
/**
|
||||||
|
* Removes all current stacks
|
||||||
|
*/
|
||||||
|
clearStacks() {
|
||||||
|
stackMap = {};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Create a stack from an image set, as well as add in the metadata on a per image bases.
|
||||||
|
* @param study The study who's metadata will be added
|
||||||
|
* @param displaySet The set of images to make the stack from
|
||||||
|
* @return {Array} Array with image IDs
|
||||||
|
*/
|
||||||
|
makeAndAddStack(study, displaySet) {
|
||||||
|
return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Find a stack from the currently created stacks.
|
||||||
|
* @param displaySetInstanceUid The UID of the stack to find.
|
||||||
|
* @returns {*} undefined if not found, otherwise the stack object is returned.
|
||||||
|
*/
|
||||||
|
findStack(displaySetInstanceUid) {
|
||||||
|
return stackMap[displaySetInstanceUid];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Gets the underlying map of displaySetInstanceUid to stack object.
|
||||||
|
* WARNING: Do not change this object. It directly affects the manager.
|
||||||
|
* @returns {{}} map of displaySetInstanceUid -> stack.
|
||||||
|
*/
|
||||||
|
getAllStacks() {
|
||||||
|
return stackMap;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Adds in a callback to be called on a stack being added / updated.
|
||||||
|
* @param callback must accept at minimum one argument,
|
||||||
|
* which is the stack that was added / updated.
|
||||||
|
*/
|
||||||
|
addStackUpdatedCallback(callback) {
|
||||||
|
if (typeof callback !== 'function') {
|
||||||
|
throw new OHIFError('callback must be provided as a function');
|
||||||
|
}
|
||||||
|
stackUpdatedCallbacks.push(callback);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Return configuration
|
||||||
|
*/
|
||||||
|
getConfiguration() {
|
||||||
|
return configuration;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set configuration, in order to provide compatibility
|
||||||
|
* with other systems by overriding this functions
|
||||||
|
* @param {Object} config object with functions to be overrided
|
||||||
|
*
|
||||||
|
* For now, only makeAndAddStack can be overrided
|
||||||
|
*/
|
||||||
|
setConfiguration(config) {
|
||||||
|
configuration = config;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { StackManager };
|
||||||
90
Packages/ohif-viewerbase/client/lib/classes/DisplaySet.js
Normal file
90
Packages/ohif-viewerbase/client/lib/classes/DisplaySet.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Random } from 'meteor/random';
|
||||||
|
import { Metadata } from './metadata/Metadata';
|
||||||
|
import { InstanceMetadata } from './metadata/InstanceMetadata';
|
||||||
|
|
||||||
|
export class DisplaySet {
|
||||||
|
|
||||||
|
constructor(instances) {
|
||||||
|
this._uid = Random.id();
|
||||||
|
this._attributes = {};
|
||||||
|
this._instances = [];
|
||||||
|
|
||||||
|
if (instances instanceof Array) {
|
||||||
|
instances.forEach( instance => this.addInstance(instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUID() {
|
||||||
|
return this._uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the number of instances within the current display set.
|
||||||
|
* @returns {number} The number of instances in the current display set.
|
||||||
|
*/
|
||||||
|
getInstanceCount() {
|
||||||
|
return this._instances.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an instance by index.
|
||||||
|
* @param {number} index An integer representing a list index.
|
||||||
|
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
|
||||||
|
*/
|
||||||
|
getInstanceByIndex(index) {
|
||||||
|
let found; // undefined by default...
|
||||||
|
if (Metadata.isValidIndex(index)) {
|
||||||
|
found = this._instances[index];
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the supplied callback for each instance in the current display set passing
|
||||||
|
* two arguments: instance (InstanceMetadata) and index (the integer index of the instance within the current display set)
|
||||||
|
* @param {function} callback The callback function which will be invoked for each instance in the display set.
|
||||||
|
* @returns {undefined} Nothing is returned.
|
||||||
|
*/
|
||||||
|
forEachInstance(callback) {
|
||||||
|
if (Metadata.isValidCallback(callback)) {
|
||||||
|
this._instances.forEach((instance, index) => {
|
||||||
|
callback.call(null, instance, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append an instance to the current series.
|
||||||
|
* @param {InstanceMetadata} instance The instance to be added to the current series.
|
||||||
|
* @returns {boolean} Returns true on success, false otherwise.
|
||||||
|
*/
|
||||||
|
addInstance(instance) {
|
||||||
|
let result = false;
|
||||||
|
if (instance instanceof InstanceMetadata) {
|
||||||
|
this._instances.push(instance);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute(attribute, value) {
|
||||||
|
this._attributes[attribute] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(attribute) {
|
||||||
|
return this._attributes[attribute];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttributes(attributes) {
|
||||||
|
if (typeof attributes === 'object' && attributes !== null) {
|
||||||
|
const _attributes = this._attributes;
|
||||||
|
const hasOwn = Object.prototype.hasOwnProperty;
|
||||||
|
for (let attribute in attributes) {
|
||||||
|
if (hasOwn.call(attributes, attribute)) {
|
||||||
|
_attributes[attribute] = attributes[attribute];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
Packages/ohif-viewerbase/client/lib/classes/ImageSet.js
Normal file
50
Packages/ohif-viewerbase/client/lib/classes/ImageSet.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Random } from 'meteor/random';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines an ImageSet object which will be used across the viewer. This object represents
|
||||||
|
* a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the
|
||||||
|
* main attributes (images and uid) it allows additional attributes to be appended to it (currently
|
||||||
|
* indiscriminately, but this should be changed).
|
||||||
|
*/
|
||||||
|
export class ImageSet {
|
||||||
|
|
||||||
|
constructor(images) {
|
||||||
|
|
||||||
|
if (Array.isArray(images) !== true) {
|
||||||
|
throw new TypeError('ImageSet expects an array of images...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main ImageSet attributes
|
||||||
|
this.images = images; // Array of images
|
||||||
|
this.uid = Random.id(); // Unique ID of the instance
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute(attribute, value) {
|
||||||
|
this[attribute] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(attribute) {
|
||||||
|
return this[attribute];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttributes(attributes) {
|
||||||
|
if (typeof attributes === 'object' && attributes !== null) {
|
||||||
|
const imageSet = this, hasOwn = Object.prototype.hasOwnProperty;
|
||||||
|
for (let attribute in attributes) {
|
||||||
|
if (hasOwn.call(attributes, attribute)) {
|
||||||
|
imageSet[attribute] = attributes[attribute];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getImage(index) {
|
||||||
|
return this.images[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy(sortingCallback) {
|
||||||
|
return this.images.sort(sortingCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
589
Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js
Normal file
589
Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
import { Blaze } from 'meteor/blaze';
|
||||||
|
import { Session } from 'meteor/session';
|
||||||
|
import { Tracker } from 'meteor/tracker';
|
||||||
|
import { Template } from 'meteor/templating';
|
||||||
|
import { Random } from 'meteor/random';
|
||||||
|
import { _ } from 'meteor/underscore';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
|
// Displays Series in Viewports given a Protocol and list of Studies
|
||||||
|
export class LayoutManager {
|
||||||
|
constructor(parentNode, studies) {
|
||||||
|
OHIF.log.info('LayoutManager constructor');
|
||||||
|
|
||||||
|
this.parentNode = parentNode;
|
||||||
|
this.studies = studies;
|
||||||
|
this.viewportData = [];
|
||||||
|
this.layoutTemplateName = 'gridLayout';
|
||||||
|
this.layoutProps = {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isZoomed = false;
|
||||||
|
|
||||||
|
const updateSessionFn = () => Tracker.afterFlush(() => Session.set('LayoutManagerUpdated', Random.id()));
|
||||||
|
this.updateSession = _.throttle(updateSessionFn, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNumberOfViewports() {
|
||||||
|
return this.layoutProps.rows * this.layoutProps.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultViewportData() {
|
||||||
|
OHIF.log.info('LayoutManager setDefaultViewportData');
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// Get the number of vieports to be rendered
|
||||||
|
const viewportsAmount = this.getNumberOfViewports();
|
||||||
|
|
||||||
|
// Store the old viewport data and reset the current
|
||||||
|
const oldViewportData = self.viewportData;
|
||||||
|
|
||||||
|
// Get the studies and display sets sequence map
|
||||||
|
const sequenceMap = this.getDisplaySetSequenceMap();
|
||||||
|
|
||||||
|
// Check if the display sets are sequenced
|
||||||
|
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
|
||||||
|
|
||||||
|
// Define the current viewport index and the viewport data array
|
||||||
|
let currentViewportIndex = 0;
|
||||||
|
if (viewportsAmount > oldViewportData.length && oldViewportData.length && isSequenced) {
|
||||||
|
// Keep the displayed display sets
|
||||||
|
self.viewportData = oldViewportData;
|
||||||
|
currentViewportIndex = oldViewportData.length;
|
||||||
|
}
|
||||||
|
else if (viewportsAmount <= oldViewportData.length) {
|
||||||
|
// Reduce the original displayed display sets
|
||||||
|
self.viewportData = oldViewportData.slice(0, viewportsAmount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Reset all display sets
|
||||||
|
self.viewportData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the display sets for the viewer studies
|
||||||
|
let displaySets = [];
|
||||||
|
this.studies.forEach(study => {
|
||||||
|
study.series.forEach(displaySet => {
|
||||||
|
displaySet.images.length && displaySets.push(displaySet);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the display sets that will be appended to the current ones
|
||||||
|
let appendix;
|
||||||
|
const currentLength = self.viewportData.length;
|
||||||
|
if (currentLength) {
|
||||||
|
// TODO: isolate displaySets array by study (maybe a map?)
|
||||||
|
const beginIndex = sequenceMap.values().next().value[0].displaySetIndex + currentLength;
|
||||||
|
const endIndex = beginIndex + (viewportsAmount - currentLength);
|
||||||
|
appendix = displaySets.slice(beginIndex, endIndex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Get available display sets from the first to the grid size
|
||||||
|
appendix = displaySets.slice(0, viewportsAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the additional data based on the appendix
|
||||||
|
const additionalData = [];
|
||||||
|
appendix.forEach((displaySet, index) => {
|
||||||
|
additionalData.push({
|
||||||
|
viewportIndex: currentViewportIndex + index,
|
||||||
|
studyInstanceUid: displaySet.studyInstanceUid,
|
||||||
|
seriesInstanceUid: displaySet.seriesInstanceUid,
|
||||||
|
displaySetInstanceUid: displaySet.displaySetInstanceUid,
|
||||||
|
sopInstanceUid: displaySet.images[0].sopInstanceUid
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append the additional data with the viewport data
|
||||||
|
self.viewportData = self.viewportData.concat(additionalData);
|
||||||
|
|
||||||
|
// Push empty objects if the amount is lesser than the grid size
|
||||||
|
while (self.viewportData.length < viewportsAmount) {
|
||||||
|
self.viewportData.push({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateViewports() {
|
||||||
|
OHIF.log.info('LayoutManager updateViewports');
|
||||||
|
|
||||||
|
if (!this.viewportData ||
|
||||||
|
!this.viewportData.length ||
|
||||||
|
this.viewportData.length !== this.getNumberOfViewports()) {
|
||||||
|
this.setDefaultViewportData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageViewerViewports occasionally needs relevant layout data in order to set
|
||||||
|
// the element style of the viewport in question
|
||||||
|
const layoutProps = this.layoutProps;
|
||||||
|
const data = $.extend({
|
||||||
|
viewportData: []
|
||||||
|
}, layoutProps);
|
||||||
|
|
||||||
|
this.viewportData.forEach( viewportData => {
|
||||||
|
const viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
|
||||||
|
data.viewportData.push(viewportDataAndLayoutProps);
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutTemplate = Template[this.layoutTemplateName];
|
||||||
|
|
||||||
|
$(this.parentNode).html('');
|
||||||
|
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||||
|
|
||||||
|
this.updateSession();
|
||||||
|
|
||||||
|
this.isZoomed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function destroys and re-renders the imageViewerViewport template.
|
||||||
|
* It uses the data provided to load a new display set into the produced viewport.
|
||||||
|
*
|
||||||
|
* @param viewportIndex
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
rerenderViewportWithNewDisplaySet(viewportIndex, data) {
|
||||||
|
OHIF.log.info(`LayoutManager rerenderViewportWithNewDisplaySet: ${viewportIndex}`);
|
||||||
|
|
||||||
|
// The parent container is identified because it is later removed from the DOM
|
||||||
|
const element = $('.imageViewerViewport').eq(viewportIndex);
|
||||||
|
const container = element.parents('.viewportContainer').get(0);
|
||||||
|
|
||||||
|
// Record the current viewportIndex so this can be passed into the re-rendering call
|
||||||
|
data.viewportIndex = viewportIndex;
|
||||||
|
|
||||||
|
// Update the dictionary of loaded displaySet for the specified viewport
|
||||||
|
this.viewportData[viewportIndex] = {
|
||||||
|
viewportIndex: viewportIndex,
|
||||||
|
displaySetInstanceUid: data.displaySetInstanceUid,
|
||||||
|
seriesInstanceUid: data.seriesInstanceUid,
|
||||||
|
studyInstanceUid: data.studyInstanceUid,
|
||||||
|
renderedCallback: data.renderedCallback,
|
||||||
|
currentImageIdIndex: data.currentImageIdIndex || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the hover styling
|
||||||
|
element.find('canvas').not('.magnifyTool').removeClass('faded');
|
||||||
|
|
||||||
|
// Remove the whole template, add in the new one
|
||||||
|
const viewportContainer = element.parents('.removable');
|
||||||
|
|
||||||
|
const newViewportContainer = document.createElement('div');
|
||||||
|
newViewportContainer.className = 'removable';
|
||||||
|
|
||||||
|
// Remove the parent element of the template
|
||||||
|
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
|
||||||
|
viewportContainer.remove();
|
||||||
|
|
||||||
|
container.appendChild(newViewportContainer);
|
||||||
|
|
||||||
|
// Render and insert the template
|
||||||
|
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
|
||||||
|
|
||||||
|
this.updateSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
enlargeViewport(viewportIndex) {
|
||||||
|
OHIF.log.info(`LayoutManager enlargeViewport: ${viewportIndex}`);
|
||||||
|
|
||||||
|
if (!this.viewportData ||
|
||||||
|
!this.viewportData.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the array for later
|
||||||
|
this.previousViewportData = this.viewportData.slice(0);
|
||||||
|
|
||||||
|
const singleViewportData = $.extend({}, this.viewportData[viewportIndex]);
|
||||||
|
singleViewportData.rows = 1;
|
||||||
|
singleViewportData.columns = 1;
|
||||||
|
singleViewportData.viewportIndex = 0;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
viewportData: [singleViewportData],
|
||||||
|
rows: 1,
|
||||||
|
columns: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutTemplate = Template['gridLayout'];
|
||||||
|
|
||||||
|
$(this.parentNode).html('');
|
||||||
|
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||||
|
|
||||||
|
this.isZoomed = true;
|
||||||
|
this.zoomedViewportIndex = viewportIndex;
|
||||||
|
this.viewportData = data.viewportData;
|
||||||
|
|
||||||
|
this.updateSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPreviousLayout() {
|
||||||
|
OHIF.log.info('LayoutManager resetPreviousLayout');
|
||||||
|
|
||||||
|
if (!this.isZoomed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousViewportData[this.zoomedViewportIndex] = $.extend({}, this.viewportData[0]);
|
||||||
|
this.previousViewportData[this.zoomedViewportIndex].viewportIndex = this.zoomedViewportIndex;
|
||||||
|
this.viewportData = this.previousViewportData;
|
||||||
|
this.updateViewports();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEnlargement(viewportIndex) {
|
||||||
|
OHIF.log.info(`LayoutManager toggleEnlargement: ${viewportIndex}`);
|
||||||
|
|
||||||
|
if (this.isZoomed) {
|
||||||
|
this.resetPreviousLayout();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Don't enlarge the viewport if we only have one Viewport
|
||||||
|
// to begin with
|
||||||
|
if (this.getNumberOfViewports() > 1) {
|
||||||
|
this.enlargeViewport(viewportIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the display sets map sequence of display sets and viewports
|
||||||
|
getDisplaySetSequenceMap() {
|
||||||
|
OHIF.log.info('LayoutManager getDisplaySetSequenceMap');
|
||||||
|
|
||||||
|
// Get the viewport data list
|
||||||
|
const viewportDataList = this.viewportData;
|
||||||
|
|
||||||
|
// Create a map to control the display set sequence
|
||||||
|
const sequenceMap = new Map();
|
||||||
|
|
||||||
|
// Iterate over each viewport and register its details on the sequence map
|
||||||
|
viewportDataList.forEach((viewportData, viewportIndex) => {
|
||||||
|
// Get the current study
|
||||||
|
const currentStudy = _.findWhere(this.studies, {
|
||||||
|
studyInstanceUid: viewportData.studyInstanceUid
|
||||||
|
}) || this.studies[0];
|
||||||
|
|
||||||
|
// Get the display sets
|
||||||
|
const displaySets = currentStudy.series;
|
||||||
|
|
||||||
|
// Get the current display set
|
||||||
|
const displaySet = _.findWhere(displaySets, {
|
||||||
|
displaySetInstanceUid: viewportData.displaySetInstanceUid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the current instance index (using 9999 to sort greater than -1)
|
||||||
|
let displaySetIndex = _.indexOf(displaySets, displaySet);
|
||||||
|
displaySetIndex = displaySetIndex < 0 ? 9999 : displaySetIndex;
|
||||||
|
|
||||||
|
// Try to get a map entry for current study or create it if not present
|
||||||
|
let studyViewports = sequenceMap.get(currentStudy);
|
||||||
|
if (!studyViewports) {
|
||||||
|
studyViewports = [];
|
||||||
|
sequenceMap.set(currentStudy, studyViewports);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the viewport index and the display set index on the map
|
||||||
|
studyViewports.push({
|
||||||
|
viewportIndex,
|
||||||
|
displaySetIndex
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the generated sequence map
|
||||||
|
return sequenceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all the display sets and viewports are sequenced
|
||||||
|
isDisplaySetsSequenced(definedSequenceMap) {
|
||||||
|
OHIF.log.info('LayoutManager isDisplaySetsSequenced');
|
||||||
|
|
||||||
|
let isSequenced = true;
|
||||||
|
|
||||||
|
// Get the studies and display sets sequence map
|
||||||
|
const sequenceMap = definedSequenceMap || this.getDisplaySetSequenceMap();
|
||||||
|
|
||||||
|
sequenceMap.forEach((studyViewports, study) => {
|
||||||
|
let lastDisplaySetIndex = null;
|
||||||
|
let lastViewportIndex = null;
|
||||||
|
studyViewports.forEach(({ viewportIndex, displaySetIndex }, index) => {
|
||||||
|
// Check if the sequence is wrong
|
||||||
|
if (
|
||||||
|
displaySetIndex !== 9999 &&
|
||||||
|
lastViewportIndex !== null &&
|
||||||
|
lastDisplaySetIndex !== null &&
|
||||||
|
displaySetIndex !== null &&
|
||||||
|
(viewportIndex - 1 !== lastViewportIndex ||
|
||||||
|
displaySetIndex - 1 !== lastDisplaySetIndex)
|
||||||
|
) {
|
||||||
|
// Set the sequenced flag as false;
|
||||||
|
isSequenced = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the last viewport index
|
||||||
|
lastViewportIndex = viewportIndex;
|
||||||
|
|
||||||
|
// Update the last display set index
|
||||||
|
lastDisplaySetIndex = displaySetIndex;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return isSequenced;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if is possible to move display sets on a specific direction
|
||||||
|
canMoveDisplaySets(isNext) {
|
||||||
|
OHIF.log.info('LayoutManager canMoveDisplaySets');
|
||||||
|
|
||||||
|
// Get the setting that defines if the display set navigation is multiple
|
||||||
|
const isMultiple = OHIF.uiSettings.displaySetNavigationMultipleViewports;
|
||||||
|
|
||||||
|
// Get the setting that allow display set navigation looping over series
|
||||||
|
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
|
||||||
|
|
||||||
|
// Get the studies and display sets sequence map
|
||||||
|
const sequenceMap = this.getDisplaySetSequenceMap();
|
||||||
|
|
||||||
|
// Check if the display sets are sequenced
|
||||||
|
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
|
||||||
|
|
||||||
|
// Get Active Viewport Index if isMultiple is false
|
||||||
|
const activeViewportIndex = !isMultiple ? Session.get('activeViewport') : null;
|
||||||
|
|
||||||
|
// Check if is next and looping is blocked
|
||||||
|
if (isNext && !allowLooping) {
|
||||||
|
// Check if the end was reached
|
||||||
|
let endReached = true;
|
||||||
|
|
||||||
|
sequenceMap.forEach((studyViewports, study) => {
|
||||||
|
// Get active viewport index if isMultiple is false ortherwise get last
|
||||||
|
const studyViewport = studyViewports[activeViewportIndex !== null ? activeViewportIndex : studyViewports.length - 1];
|
||||||
|
if (!studyViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportIndex = studyViewport.displaySetIndex;
|
||||||
|
const layoutViewports = studyViewports.length;
|
||||||
|
const amount = study.displaySets.length;
|
||||||
|
const move = !isMultiple ? 1 : ((amount % layoutViewports) || layoutViewports);
|
||||||
|
const lastStepIndex = amount - move;
|
||||||
|
|
||||||
|
// 9999 for index means empty viewport, see getDisplaySetSequenceMap function
|
||||||
|
if (viewportIndex !== 9999 && viewportIndex !== lastStepIndex) {
|
||||||
|
endReached = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return false if end is not reached yet
|
||||||
|
if ((!isMultiple || isSequenced) && endReached) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if is previous and looping is blocked
|
||||||
|
if (!isNext && !allowLooping) {
|
||||||
|
// Check if the begin was reached
|
||||||
|
let beginReached = true;
|
||||||
|
|
||||||
|
if(activeViewportIndex >= 0) {
|
||||||
|
sequenceMap.forEach((studyViewports, study) => {
|
||||||
|
// Get active viewport index if isMultiple is false ortherwise get first
|
||||||
|
const studyViewport = studyViewports[activeViewportIndex !== null ? activeViewportIndex : 0];
|
||||||
|
if (!studyViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportIndex = studyViewport.displaySetIndex;
|
||||||
|
const layoutViewports = studyViewports.length;
|
||||||
|
|
||||||
|
// 9999 for index means empty viewport, see getDisplaySetSequenceMap function
|
||||||
|
if (viewportIndex !== 9999 && viewportIndex - layoutViewports !== -layoutViewports) {
|
||||||
|
beginReached = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false if begin is not reached yet
|
||||||
|
if ((!isMultiple || isSequenced) && beginReached) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move display sets forward or backward in the given viewport index
|
||||||
|
moveSingleViewportDisplaySets(viewportIndex, isNext) {
|
||||||
|
OHIF.log.info(`LayoutManager moveSingleViewportDisplaySets: ${viewportIndex}`);
|
||||||
|
|
||||||
|
// Get the setting that allow display set navigation looping over series
|
||||||
|
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
|
||||||
|
|
||||||
|
// Get the selected viewport data
|
||||||
|
const viewportData = this.viewportData[viewportIndex];
|
||||||
|
|
||||||
|
// Get the current study
|
||||||
|
const currentStudy = _.findWhere(this.studies, {
|
||||||
|
studyInstanceUid: viewportData.studyInstanceUid
|
||||||
|
}) || this.studies[0];
|
||||||
|
|
||||||
|
// Get the display sets
|
||||||
|
const displaySets = currentStudy.displaySets;
|
||||||
|
|
||||||
|
// Get the current display set
|
||||||
|
const currentDisplaySet = _.findWhere(displaySets, {
|
||||||
|
displaySetInstanceUid: viewportData.displaySetInstanceUid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the new index and ensure that it will exists in display sets
|
||||||
|
let newIndex = _.indexOf(displaySets, currentDisplaySet);
|
||||||
|
if (isNext) {
|
||||||
|
newIndex++;
|
||||||
|
if (newIndex >= displaySets.length) {
|
||||||
|
// Stop here if looping is not allowed
|
||||||
|
if (!allowLooping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newIndex--;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
// Stop here if looping is not allowed
|
||||||
|
if (!allowLooping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndex = displaySets.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the display set data for the new index
|
||||||
|
const newDisplaySetData = displaySets[newIndex];
|
||||||
|
|
||||||
|
// Rerender the viewport using the new display set data
|
||||||
|
this.rerenderViewportWithNewDisplaySet(viewportIndex, newDisplaySetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move multiple display sets forward or backward in all viewports
|
||||||
|
moveMultipleViewportDisplaySets(isNext) {
|
||||||
|
OHIF.log.info('LayoutManager moveMultipleViewportDisplaySets');
|
||||||
|
|
||||||
|
// Get the setting that allow display set navigation looping over series
|
||||||
|
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
|
||||||
|
|
||||||
|
// Create a map to control the display set sequence
|
||||||
|
const sequenceMap = new this.getDisplaySetSequenceMap();
|
||||||
|
|
||||||
|
// Check if the display sets are sequenced
|
||||||
|
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
|
||||||
|
|
||||||
|
const displaySetsToRender = [];
|
||||||
|
|
||||||
|
// Iterate over the studies map and move its display sets
|
||||||
|
sequenceMap.forEach((studyViewports, study) => {
|
||||||
|
// Sort the viewports on the study by the display set index
|
||||||
|
studyViewports.sort((a, b) => a.displaySetIndex > b.displaySetIndex);
|
||||||
|
|
||||||
|
// Get the study display sets
|
||||||
|
const displaySets = study.displaySets;
|
||||||
|
|
||||||
|
// Calculate the base index
|
||||||
|
const firstIndex = studyViewports[0].displaySetIndex;
|
||||||
|
const steps = studyViewports.length;
|
||||||
|
const rest = firstIndex % steps;
|
||||||
|
let baseIndex = rest ? firstIndex - rest : firstIndex;
|
||||||
|
const direction = isNext ? 1 : -1;
|
||||||
|
baseIndex += steps * direction;
|
||||||
|
|
||||||
|
const amount = displaySets.length;
|
||||||
|
|
||||||
|
// Check if the indexes are sequenced or will overflow the array bounds
|
||||||
|
if (baseIndex >= amount) {
|
||||||
|
const move = (amount % steps) || steps;
|
||||||
|
const lastStepIndex = amount - move;
|
||||||
|
if (firstIndex + steps !== lastStepIndex + steps) {
|
||||||
|
// Reset the index if the display sets are sequenced but shifted
|
||||||
|
baseIndex = lastStepIndex;
|
||||||
|
} else if (!allowLooping) {
|
||||||
|
// Stop here if looping is not allowed
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Start over the series if looping is allowed
|
||||||
|
baseIndex = 0;
|
||||||
|
}
|
||||||
|
} else if (baseIndex < 0) {
|
||||||
|
if (firstIndex > 0) {
|
||||||
|
// Reset the index if the display sets are sequenced but shifted
|
||||||
|
baseIndex = 0;
|
||||||
|
} else if (!allowLooping) {
|
||||||
|
// Stop here if looping is not allowed
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Go to the series' end if looping is allowed
|
||||||
|
baseIndex = (amount - 1) - ((amount - 1) % steps);
|
||||||
|
}
|
||||||
|
} else if (!isSequenced) {
|
||||||
|
// Reset the sequence if indexes are not sequenced
|
||||||
|
baseIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the current study viewports
|
||||||
|
studyViewports.forEach(({ viewportIndex }, index) => {
|
||||||
|
// Get the new displaySet index to be rendered in viewport
|
||||||
|
const newIndex = baseIndex + index;
|
||||||
|
|
||||||
|
// Get the display set data for the new index
|
||||||
|
const displaySetData = displaySets[newIndex] || {};
|
||||||
|
|
||||||
|
// Add the current display set that on the render list
|
||||||
|
displaySetsToRender.push(displaySetData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the display sets
|
||||||
|
const sortingFunction = OHIF.utils.sortBy({
|
||||||
|
name: 'studyInstanceUid'
|
||||||
|
}, {
|
||||||
|
name: 'instanceNumber'
|
||||||
|
}, {
|
||||||
|
name: 'seriesNumber'
|
||||||
|
});
|
||||||
|
displaySetsToRender.sort((a, b) => sortingFunction(a, b));
|
||||||
|
|
||||||
|
// Iterate over each display set data and render on its respective viewport
|
||||||
|
displaySetsToRender.forEach((data, index) => {
|
||||||
|
this.rerenderViewportWithNewDisplaySet(index, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move display sets forward or backward
|
||||||
|
moveDisplaySets(isNext) {
|
||||||
|
OHIF.log.info('LayoutManager moveDisplaySets');
|
||||||
|
|
||||||
|
//Check if navigation is on a single or multiple viewports
|
||||||
|
if (OHIF.uiSettings.displaySetNavigationMultipleViewports) {
|
||||||
|
// Move display sets on multiple viewports
|
||||||
|
this.moveMultipleViewportDisplaySets(isNext);
|
||||||
|
} else {
|
||||||
|
// Get the selected viewport index
|
||||||
|
const viewportIndex = Session.get('activeViewport');
|
||||||
|
|
||||||
|
// Move display sets on a single viewport
|
||||||
|
this.moveSingleViewportDisplaySets(viewportIndex, isNext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isStudyLoadedIntoViewport(studyInstanceUid, viewportIndex) {
|
||||||
|
return (this.viewportData.find(item => item.studyInstanceUid === studyInstanceUid && item.viewportIndex === viewportIndex) !== void 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMultipleLayout() {
|
||||||
|
return this.layoutProps.row !== 1 && this.layoutProps.columns !== 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
14
Packages/ohif-viewerbase/client/lib/classes/OHIFError.js
Normal file
14
Packages/ohif-viewerbase/client/lib/classes/OHIFError.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// @TODO: improve this object
|
||||||
|
/**
|
||||||
|
* Objects to be used to throw errors, specially
|
||||||
|
* in Trackers functions (afterFlush, Flush).
|
||||||
|
*/
|
||||||
|
export class OHIFError extends Error {
|
||||||
|
|
||||||
|
constructor(message) {
|
||||||
|
super();
|
||||||
|
this.message = message;
|
||||||
|
this.stack = (new Error()).stack;
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import { Session } from 'meteor/session';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
import { getInstanceClassDefaultViewport } from '../instanceClassSpecificViewport';
|
||||||
|
|
||||||
|
// Manage resizing viewports triggered by window resize
|
||||||
|
export class ResizeViewportManager {
|
||||||
|
constructor() {
|
||||||
|
OHIF.log.info('ResizeViewportManager');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reposition Study Series Quick Switch based whether side bars are opened or not
|
||||||
|
repositionStudySeriesQuickSwitch() {
|
||||||
|
OHIF.log.info('ResizeViewportManager repositionStudySeriesQuickSwitch');
|
||||||
|
|
||||||
|
const activeTab = Session.get('activeContentId');
|
||||||
|
if(activeTab === 'viewerTab') {
|
||||||
|
const nViewports = OHIF.viewerbase.layoutManager.viewportData.length;
|
||||||
|
|
||||||
|
if(nViewports && nViewports > 1) {
|
||||||
|
const viewer = $('#viewer');
|
||||||
|
const leftSidebar = viewer.find('.sidebar-left.sidebar-open');
|
||||||
|
const rightSidebar = viewer.find('.sidebar-right.sidebar-open');
|
||||||
|
|
||||||
|
const leftQuickSwitch = $('.quickSwitchWrapper.left');
|
||||||
|
const rightQuickSwitch = $('.quickSwitchWrapper.right');
|
||||||
|
|
||||||
|
const hasLeftSidebar = leftSidebar.length > 0;
|
||||||
|
const hasRightSidebar = rightSidebar.length > 0;
|
||||||
|
|
||||||
|
rightQuickSwitch.removeClass('left-sidebar-only');
|
||||||
|
leftQuickSwitch.removeClass('right-sidebar-only');
|
||||||
|
|
||||||
|
let leftOffset = 0;
|
||||||
|
|
||||||
|
if(hasLeftSidebar) {
|
||||||
|
leftOffset = ( leftSidebar.width()/$(window).width() ) * 100;
|
||||||
|
|
||||||
|
if(!hasRightSidebar) {
|
||||||
|
rightQuickSwitch.addClass('left-sidebar-only');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasRightSidebar && !hasLeftSidebar) {
|
||||||
|
leftQuickSwitch.addClass('right-sidebar-only');
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftPosition = ( ($('#imageViewerViewports').width() / nViewports) / $(window).width() ) * 100 + leftOffset;
|
||||||
|
const rightPosition = 100 - leftPosition;
|
||||||
|
|
||||||
|
leftQuickSwitch.css('right', rightPosition + '%');
|
||||||
|
rightQuickSwitch.css('left', leftPosition + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relocate dialogs positions
|
||||||
|
relocateDialogs(){
|
||||||
|
OHIF.log.info('ResizeViewportManager relocateDialogs');
|
||||||
|
|
||||||
|
const bottomRightDialogs = $('#cineDialog, #annotationDialog, #textMarkerOptionsDialog');
|
||||||
|
bottomRightDialogs.css({
|
||||||
|
top: '', // This removes the CSS property completely
|
||||||
|
left: '',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerDialogs = $('.draggableDialog').not(bottomRightDialogs);
|
||||||
|
|
||||||
|
centerDialogs.css({
|
||||||
|
top: 0,
|
||||||
|
left:0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize viewport scrollbars
|
||||||
|
resizeScrollbars(element) {
|
||||||
|
OHIF.log.info('ResizeViewportManager resizeScrollbars');
|
||||||
|
|
||||||
|
const currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
|
||||||
|
const imageControls = currentOverlay.find('.imageControls');
|
||||||
|
currentOverlay.find('.imageControls').height($(element).height());
|
||||||
|
|
||||||
|
// Set it's width to its parent's height
|
||||||
|
// (because webkit is stupid and can't style vertical sliders)
|
||||||
|
const scrollbar = currentOverlay.find('#scrollbar');
|
||||||
|
scrollbar.height(scrollbar.parent().height() - 20);
|
||||||
|
|
||||||
|
const currentImageSlider = currentOverlay.find('#imageSlider');
|
||||||
|
const overlayHeight = currentImageSlider.parent().height();
|
||||||
|
const browserInfo = cornerstoneTools.getBrowserInfo();
|
||||||
|
|
||||||
|
if (browserInfo.indexOf('IE') > -1) {
|
||||||
|
currentImageSlider.height(overlayHeight);
|
||||||
|
} else {
|
||||||
|
currentImageSlider.width(overlayHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize a single viewport element
|
||||||
|
resizeViewportElement(element) {
|
||||||
|
let enabledElement;
|
||||||
|
try {
|
||||||
|
enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
} catch(error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.resize(element, true);
|
||||||
|
|
||||||
|
if (enabledElement.fitToWindow === false) {
|
||||||
|
const imageId = enabledElement.image.imageId;
|
||||||
|
const instance = cornerstoneTools.metaData.get('instance', imageId);
|
||||||
|
const instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
|
||||||
|
cornerstone.setViewport(element, instanceClassViewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize each viewport element
|
||||||
|
resizeViewportElements() {
|
||||||
|
this.relocateDialogs();
|
||||||
|
|
||||||
|
const viewportResizeTimer = setTimeout(() => {
|
||||||
|
this.repositionStudySeriesQuickSwitch();
|
||||||
|
|
||||||
|
const elements = $('.imageViewerViewport').not('.empty');
|
||||||
|
elements.each((index, element) => {
|
||||||
|
this.resizeViewportElement(element);
|
||||||
|
this.resizeScrollbars(element);
|
||||||
|
});
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to override resizeViewportElements function
|
||||||
|
setResizeViewportElement(resizeViewportElements) {
|
||||||
|
this.resizeViewportElements = resizeViewportElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
handleResize() {
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
OHIF.log.info('ResizeViewportManager resizeViewportElements');
|
||||||
|
this.resizeViewportElements();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,330 @@
|
|||||||
|
import { Random } from 'meteor/random';
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
|
|
||||||
|
const FUNCTION = 'function';
|
||||||
|
const OBJECT = 'object';
|
||||||
|
const STRING = 'string';
|
||||||
|
const PROPERTY_SEPARATOR = '.';
|
||||||
|
const ORDER_ASC = 'asc';
|
||||||
|
const ORDER_DESC = 'desc';
|
||||||
|
const MIN_COUNT = 0x00000000;
|
||||||
|
const MAX_COUNT = 0x7FFFFFFF;
|
||||||
|
|
||||||
|
export class TypeSafeCollection {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._operationCount = new ReactiveVar(MIN_COUNT);
|
||||||
|
this._elementList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
_invalidate() {
|
||||||
|
let count = this._operationCount.get();
|
||||||
|
this._operationCount.set(count < MAX_COUNT ? count + 1 : MIN_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_elements(silent) {
|
||||||
|
(silent === true || this._operationCount.get());
|
||||||
|
return this._elementList;
|
||||||
|
}
|
||||||
|
|
||||||
|
_elementWithPayload(payload, silent) {
|
||||||
|
return this._elements(silent).find(item => item.payload === payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
_elementWithId(id, silent) {
|
||||||
|
return this._elements(silent).find(item => item.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an object's property value by name
|
||||||
|
* @param {Object} object The object we want read the property from...
|
||||||
|
* @param {String} propertyName The property to be read (e.g., 'address.street.name' or 'address.street.number'
|
||||||
|
* to read object.address.street.name or object.address.street.number);
|
||||||
|
* @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached.
|
||||||
|
*/
|
||||||
|
static getPropertyValue(object, propertyName) {
|
||||||
|
let propertyValue;
|
||||||
|
if (typeof object === OBJECT && object !== null && typeof propertyName === STRING) {
|
||||||
|
let fragments = propertyName.split(PROPERTY_SEPARATOR),
|
||||||
|
fragmentCount = fragments.length;
|
||||||
|
if (fragmentCount > 0) {
|
||||||
|
let firstFragment = fragments[0],
|
||||||
|
remainingFragments = fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null;
|
||||||
|
propertyValue = object[firstFragment];
|
||||||
|
if (remainingFragments !== null) {
|
||||||
|
propertyValue = TypeSafeCollection.getPropertyValue(propertyValue, remainingFragments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return propertyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a sorting specifier is valid.
|
||||||
|
* A valid sorting specifier consists of an array of arrays being each subarray a pair
|
||||||
|
* in the format ["property name", "sorting order"].
|
||||||
|
* The following exemple can be used to sort studies by "date"" and use "time" to break ties in descending order.
|
||||||
|
* [ [ 'study.date', 'desc' ], [ 'study.time', 'desc' ] ]
|
||||||
|
* @param {Array} specifiers The sorting specifier to be tested.
|
||||||
|
* @returns {boolean} Returns true if the specifiers are valid, false otherwise.
|
||||||
|
*/
|
||||||
|
static isValidSortingSpecifier(specifiers) {
|
||||||
|
let result = true;
|
||||||
|
if (specifiers instanceof Array && specifiers.length > 0) {
|
||||||
|
for (let i = specifiers.length - 1; i >= 0; i--) {
|
||||||
|
const item = specifiers[i];
|
||||||
|
if (item instanceof Array) {
|
||||||
|
const property = item[0];
|
||||||
|
const order = item[1];
|
||||||
|
if (typeof property === STRING && (order === ORDER_ASC || order === ORDER_DESC)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array based on sorting specifier options.
|
||||||
|
* @param {Array} list The that needs to be sorted.
|
||||||
|
* @param {Array} specifiers An array of specifiers. Please read isValidSortingSpecifier method definition for further details.
|
||||||
|
* @returns {void} No value is returned. The array is sorted in place.
|
||||||
|
*/
|
||||||
|
static sortListBy(list, specifiers) {
|
||||||
|
const thisClass = TypeSafeCollection;
|
||||||
|
if (list instanceof Array && thisClass.isValidSortingSpecifier(specifiers)) {
|
||||||
|
const getPropertyValue = thisClass.getPropertyValue;
|
||||||
|
const specifierCount = specifiers.length;
|
||||||
|
list.sort((a, b) => {
|
||||||
|
let index = 0;
|
||||||
|
while (index < specifierCount) {
|
||||||
|
const specifier = specifiers[index];
|
||||||
|
const property = specifier[0];
|
||||||
|
const order = specifier[1] === ORDER_DESC ? -1 : 1;
|
||||||
|
const aValue = getPropertyValue(a, property);
|
||||||
|
const bValue = getPropertyValue(b, property);
|
||||||
|
// @TODO: should we check for the types being compared, like:
|
||||||
|
// ~~ if (typeof aValue !== typeof bValue) continue;
|
||||||
|
// Not sure because dates, for example, can be correctly compared to numbers...
|
||||||
|
if (aValue < bValue) {
|
||||||
|
return order * -1;
|
||||||
|
}
|
||||||
|
if (aValue > bValue) {
|
||||||
|
return order * 1;
|
||||||
|
}
|
||||||
|
if (++index >= specifierCount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('TypeSafeCollection::sortListBy Invalid Arguments');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
updateById(id, payload) {
|
||||||
|
let result = false,
|
||||||
|
found = this._elementWithPayload(payload, true);
|
||||||
|
if (found) {
|
||||||
|
// nothing to do since the element is already in the collection...
|
||||||
|
if (found.id === id) {
|
||||||
|
// set result to true since the ids match...
|
||||||
|
result = true;
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found = this._elementWithId(id, true);
|
||||||
|
if (found) {
|
||||||
|
found.payload = payload;
|
||||||
|
result = true;
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(payload) {
|
||||||
|
let result = false,
|
||||||
|
found = this._elementWithPayload(payload, true);
|
||||||
|
if (found) {
|
||||||
|
// nothing to do since the element is already in the collection...
|
||||||
|
result = true;
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(payload) {
|
||||||
|
let id = null,
|
||||||
|
found = this._elementWithPayload(payload, true);
|
||||||
|
if (!found) {
|
||||||
|
id = Random.id();
|
||||||
|
this._elements(true).push({ id, payload });
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll() {
|
||||||
|
let all = this._elements(true),
|
||||||
|
length = all.length;
|
||||||
|
for (let i = length - 1; i >= 0; i--) {
|
||||||
|
let item = all[i];
|
||||||
|
delete item.id;
|
||||||
|
delete item.payload;
|
||||||
|
all[i] = null;
|
||||||
|
}
|
||||||
|
all.splice(0, length);
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(propertyMap) {
|
||||||
|
let found = this.findAllBy(propertyMap),
|
||||||
|
foundCount = found.length,
|
||||||
|
removed = [];
|
||||||
|
if (foundCount > 0) {
|
||||||
|
const all = this._elements(true);
|
||||||
|
for (let i = foundCount - 1; i >= 0; i--) {
|
||||||
|
let item = found[i];
|
||||||
|
all.splice(item[2], 1);
|
||||||
|
removed.push(item[0]);
|
||||||
|
}
|
||||||
|
this._invalidate();
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementId(payload) {
|
||||||
|
let found = this._elementWithPayload(payload);
|
||||||
|
return found && found.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id) {
|
||||||
|
let found = this._elementWithId(id);
|
||||||
|
return found && found.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexOfElement(payload) {
|
||||||
|
return this._elements().indexOf(this._elementWithPayload(payload, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
indexOfId(id) {
|
||||||
|
return this._elements().indexOf(this._elementWithId(id, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementByIndex(index) {
|
||||||
|
let found = ((this._elements())[index >= 0 ? index : -1]);
|
||||||
|
return found && found.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the reactive var will not be notified if no valid callback is supplied...
|
||||||
|
find(callback) {
|
||||||
|
let found;
|
||||||
|
if (typeof callback === FUNCTION) {
|
||||||
|
found = this._elements().find((item, index) => {
|
||||||
|
return callback.call(this, item.payload, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return found && found.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the reactive var will not be notified if no valid property map is supplied...
|
||||||
|
findBy(propertyMap) {
|
||||||
|
let found;
|
||||||
|
if (typeof propertyMap === OBJECT && propertyMap !== null) {
|
||||||
|
const hasOwn = Object.prototype.hasOwnProperty;
|
||||||
|
found = this._elements().find(item => {
|
||||||
|
const payload = item.payload;
|
||||||
|
for (let propertyName in propertyMap) {
|
||||||
|
if (hasOwn.call(propertyMap, propertyName)) {
|
||||||
|
if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return found && found.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all elements that match a specified property map.
|
||||||
|
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
|
||||||
|
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
|
||||||
|
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
|
||||||
|
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
|
||||||
|
* but it's not valid, an exception will be thrown.
|
||||||
|
* @returns {Array} An array with all elements stored in the collection.
|
||||||
|
*/
|
||||||
|
findAllBy(propertyMap, options) {
|
||||||
|
let found = [];
|
||||||
|
if (typeof propertyMap === OBJECT && propertyMap !== null) {
|
||||||
|
const hasOwn = Object.prototype.hasOwnProperty;
|
||||||
|
this._elements().forEach((item, index) => {
|
||||||
|
const payload = item.payload;
|
||||||
|
for (let propertyName in propertyMap) {
|
||||||
|
if (hasOwn.call(propertyMap, propertyName)) {
|
||||||
|
if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) {
|
||||||
|
return; // skip this element since it does not match the criteria...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match! Add it to the found list...
|
||||||
|
found.push([ payload, item.id, index ]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options instanceof Object && 'sort' in options) {
|
||||||
|
TypeSafeCollection.sortListBy(found, options.sort);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the reactive var will not be notified if no valid callback is supplied...
|
||||||
|
forEach(callback) {
|
||||||
|
if (typeof callback === FUNCTION) {
|
||||||
|
this._elements().forEach((item, index) => {
|
||||||
|
callback.call(this, item.payload, item.id, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of elements currently in the collection.
|
||||||
|
* @returns {number} The current number of elements in the collection.
|
||||||
|
*/
|
||||||
|
count() {
|
||||||
|
return this._elements().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list with all elements of the collection optionally sorted by a sorting specifier criteria.
|
||||||
|
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
|
||||||
|
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
|
||||||
|
* but it's not valid, an exception will be thrown.
|
||||||
|
* @returns {Array} An array with all elements stored in the collection.
|
||||||
|
*/
|
||||||
|
all(options) {
|
||||||
|
let list = this._elements().map(item => item.payload);
|
||||||
|
if (options instanceof Object && 'sort' in options) {
|
||||||
|
TypeSafeCollection.sortListBy(list, options.sort);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
import { Metadata } from './Metadata';
|
||||||
|
|
||||||
|
const UNDEFINED = 'undefined';
|
||||||
|
const NUMBER = 'number';
|
||||||
|
const STRING = 'string';
|
||||||
|
const REGEX_TAG = /^x[0-9a-f]{8}$/;
|
||||||
|
|
||||||
|
export class InstanceMetadata extends Metadata {
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
super(data);
|
||||||
|
this._sopInstanceUID = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SOPInstanceUID of the current instance.
|
||||||
|
*/
|
||||||
|
getSOPInstanceUID() {
|
||||||
|
return this._sopInstanceUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Improve this... (E.g.: blob data)
|
||||||
|
getStringValue(tagOrProperty, index, defaultValue) {
|
||||||
|
let value = this.getRawValue(tagOrProperty, defaultValue);
|
||||||
|
|
||||||
|
if (typeof value !== STRING && typeof value !== UNDEFINED) {
|
||||||
|
value = value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Improve this... (E.g.: blob data)
|
||||||
|
getFloatValue(tagOrProperty, index, defaultValue) {
|
||||||
|
let value = this.getRawValue(tagOrProperty, defaultValue);
|
||||||
|
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
||||||
|
|
||||||
|
if(value instanceof Array) {
|
||||||
|
value.forEach( (val, idx) => {
|
||||||
|
value[idx] = parseFloat(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof value === STRING ? parseFloat(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Improve this... (E.g.: blob data)
|
||||||
|
getIntValue(tagOrProperty, index, defaultValue) {
|
||||||
|
let value = this.getRawValue(tagOrProperty, defaultValue);
|
||||||
|
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
|
||||||
|
|
||||||
|
if(value instanceof Array) {
|
||||||
|
value.forEach( (val, idx) => {
|
||||||
|
value[idx] = parseFloat(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof value === STRING ? parseInt(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getRawValue(tagOrProperty, defaultValue) {
|
||||||
|
/**
|
||||||
|
* Please override this method on a specialized class.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the current instance with another one.
|
||||||
|
* @param {InstanceMetadata} instance An instance of the InstanceMetadata class.
|
||||||
|
* @returns {boolean} Returns true if both instances refer to the same instance.
|
||||||
|
*/
|
||||||
|
equals(instance) {
|
||||||
|
const self = this;
|
||||||
|
return (
|
||||||
|
instance === self ||
|
||||||
|
(
|
||||||
|
instance instanceof InstanceMetadata &&
|
||||||
|
instance.getSOPInstanceUID() === self.getSOPInstanceUID()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the tagOrProperty exists
|
||||||
|
* @param {String} tagOrProperty tag or property be checked
|
||||||
|
* @return {Boolean} True if the tag or property exists or false if doesn't
|
||||||
|
*/
|
||||||
|
tagExists(tagOrProperty) {
|
||||||
|
/**
|
||||||
|
* Please override this method
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a URI for the specified resource related to this instance (if an instance).
|
||||||
|
* @param {string} resource The name of the resource. This name cannot have any leading or
|
||||||
|
* trailing slashes
|
||||||
|
* @param {string|number} [optParameter] An optional parameter. This cannot have any leading or
|
||||||
|
* trailing slashes
|
||||||
|
* @returns {string|undefined} The constructed URI for the specified resource related to this
|
||||||
|
* instance or undefined if this sequence is a sub-sequence (i.e.: it has no related resources of
|
||||||
|
* its own).
|
||||||
|
*/
|
||||||
|
getResourceUri(resource, optParameter) {
|
||||||
|
/**
|
||||||
|
* Please override this method
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
static getTagInfo(tagOrProperty) {
|
||||||
|
let tagName = null,
|
||||||
|
propertyName = null;
|
||||||
|
|
||||||
|
if (typeof tagOrProperty === NUMBER) {
|
||||||
|
// if it's a number, build an hexadecimal representation...
|
||||||
|
tagName = 'x' + ('00000000' + tagOrProperty.toString(16)).substr(-8);
|
||||||
|
} else if (typeof tagOrProperty === STRING && REGEX_TAG.test(tagOrProperty)) {
|
||||||
|
tagName = tagOrProperty;
|
||||||
|
} else {
|
||||||
|
// use it as a property name otherwise...
|
||||||
|
propertyName = tagOrProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: map tagName to propertyName and vice versa
|
||||||
|
|
||||||
|
return { tagName, propertyName };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an value based that can be index based. This function is called by all getters. See above functions.
|
||||||
|
* - If value is a String and has indexes:
|
||||||
|
* - If undefined index: returns an array of the split values.
|
||||||
|
* - If defined index:
|
||||||
|
* - If invalid: returns defaultValue
|
||||||
|
* - If valid: returns the indexed value
|
||||||
|
* - If value is not a String, returns default value.
|
||||||
|
*/
|
||||||
|
static getIndexedValue(value, index, defaultValue) {
|
||||||
|
let result = defaultValue;
|
||||||
|
|
||||||
|
if (typeof value === STRING) {
|
||||||
|
const hasIndexValues = value.indexOf('\\') !== -1;
|
||||||
|
|
||||||
|
result = value;
|
||||||
|
|
||||||
|
if(hasIndexValues) {
|
||||||
|
const splitValues = value.split('\\');
|
||||||
|
if (Metadata.isValidIndex(index)) {
|
||||||
|
const indexedValue = splitValues[index];
|
||||||
|
|
||||||
|
result = typeof indexedValue !== STRING ? defaultValue : indexedValue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = splitValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STRING = 'string';
|
||||||
|
const NUMBER = 'number';
|
||||||
|
const FUNCTION = 'function';
|
||||||
|
|
||||||
|
export class Metadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor and Instance Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
static isValidUID(uid) {
|
||||||
|
return typeof uid === STRING && uid.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isValidIndex(index) {
|
||||||
|
return typeof index === NUMBER && index >= 0 && (index | 0) === index;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isValidCallback(callback) {
|
||||||
|
return typeof callback === FUNCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
128
Packages/ohif-viewerbase/client/lib/classes/metadata/README.md
Normal file
128
Packages/ohif-viewerbase/client/lib/classes/metadata/README.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Study Metadata Module
|
||||||
|
|
||||||
|
This module defines the API/Data-Model by which OHIF Viewerbase package and possibly distinct viewer
|
||||||
|
implementations can access studies metadata. This module does not attempt to define any means of
|
||||||
|
*loading* study metadata from any data end-point but only how the data that has been previously
|
||||||
|
loaded into the application context will be accessed by any of the routines or algorithm implementations
|
||||||
|
that need the data.
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
For various reasons like sorting, grouping or simply rendering study information, OHIF Viewerbase package
|
||||||
|
and applications depending on it usualy have the need to access study metadata. Before the current
|
||||||
|
initiative there was no uniform way of achieving that since each implementation provides study metadata
|
||||||
|
on its own specific ways. The application and the package itself needed to have a deep knowledge of the
|
||||||
|
data structures provided by the data endpoint to perform any of the operations mentioned above, meaning
|
||||||
|
that any data access code needed to be adapted or rewritten.
|
||||||
|
|
||||||
|
The intent of the current module is to provide a fairly consistent and flexible API/Data-Model by which
|
||||||
|
OHIF Viewerbase package (and different viewer implementations that depend on it) can manipulate DICOM matadata
|
||||||
|
retrieved from distinct data end points (e.g., a proprietary back end servers) in uniform ways with minor
|
||||||
|
to no modifications needed.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The current API implementation defines three classes of objects: `StudyMetadata`, `SeriesMetadata`
|
||||||
|
and `InstanceMetadata`. Inside OHIF Viewerbase package, every access to Study, Series or SOP Instance
|
||||||
|
metadata is achieved by the interface exposed by these three classes. By inheriting from them and
|
||||||
|
overriding or extending their methods, different applications with different data models can adapt
|
||||||
|
even the most peculiar data structures to the uniform interface defined by those classes. Together
|
||||||
|
these classes define a flexible and extensible data manipulation layer leaving routines and
|
||||||
|
algorithms that depend on that data untouched.
|
||||||
|
|
||||||
|
## Design Decisions & "*Protected*" Members
|
||||||
|
|
||||||
|
In order to provide for good programming practices, attributes and methods meant to be used exclusevily by
|
||||||
|
the classes themselves (for internal purposes only) were written with an initial '_' character, being thus treated
|
||||||
|
as "*protected*" members. The idea behind this practice was never to hide them from the programmers
|
||||||
|
(what makes debugging tasks painful) but only advise for something that's not part of the official public API
|
||||||
|
and thus should not be relied on. Usage of "protected" members makes the code less readable and prone to
|
||||||
|
compatibility issues.
|
||||||
|
|
||||||
|
As an example, the initial implementation of the `StudyMetadata` class defined the attribute `_studyInstanceUID`
|
||||||
|
and the method `getStudyInstanceUID`. This implies that whenever the *StudyInstanceUID* of a given study needs
|
||||||
|
to be retrieved the `getStudyInstanceUID` method should be called instead of directly accessing the
|
||||||
|
attribute `_studyInstanceUID` (which might not even be populated since `getStudyInstanceUID` can be possiblity
|
||||||
|
overriden by a subclass to satisfy specific implementation needs, leaving the attribute `_studyInstanceUID` unused).
|
||||||
|
|
||||||
|
Ex:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let studyUID = myStudy.getStudyInstanceUID(); // GOOD! :-)
|
||||||
|
[ ... ]
|
||||||
|
let otherStudyUID = anotherStudy._studyInstanceUID; // BAD... :-(
|
||||||
|
```
|
||||||
|
|
||||||
|
Another important topic is the preference of *methods* over *attributes* on the public API. This design
|
||||||
|
decision was made to ensure extensibility and flexibility (methods are extensible while standalone
|
||||||
|
attributes are not, and can be adapted – through overrides, for example – to support even the most
|
||||||
|
peculiar data models) even though the overhead a few additional function calls may incur.
|
||||||
|
|
||||||
|
## Abstract Classes
|
||||||
|
|
||||||
|
Some classes defined in this module are "*abstract*" classes (even though JavaScript does not *officially*
|
||||||
|
support such programming facility). They are *abstract* in the sense that a few methods (very important ones,
|
||||||
|
by the way) were left "*blank*" (unimplemented, or more precisely implemented as empty NOP functions) in
|
||||||
|
order to be implemented by specialized subclasses. Methods believed to be more generic were implemented in
|
||||||
|
an attempt to satify most implementation needs but nothing prevents a subclass from overriding them as well
|
||||||
|
(again, flexibility and extensibility are design goals). Most implemented methods rely on the implementation
|
||||||
|
of an unimplemented method. For example, the method `getStringValue` from `InstanceMetadata` class, which
|
||||||
|
has indeed been implemented and is meant to retrieve a metadata value as a string, internally calls the
|
||||||
|
`getRawValue` method which *was NOT implemented* and is meant to query the internal data structures for the
|
||||||
|
requested metadata value and return it *as is*. Used in that way, an application would not benefit much
|
||||||
|
from the already implemented methods. On the other hand, by simply overriding the `getRawValue` method
|
||||||
|
on a specialized class to deal with the intrinsics of its internal data structures, this very application
|
||||||
|
would now benefit from all already implemented methods.
|
||||||
|
|
||||||
|
The following code snippet tries to illustrate the idea:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
// -- InstanceMetadata.js
|
||||||
|
|
||||||
|
class InstanceMetadata {
|
||||||
|
[ ... ]
|
||||||
|
getRawValue(tagOrProperty, defaultValue) {
|
||||||
|
// Please implement this method in a specialized subclass...
|
||||||
|
}
|
||||||
|
[ ... ]
|
||||||
|
getStringValue(tagOrProperty, index, defaultValue) {
|
||||||
|
let rawValue = this.getRawValue(tagOrProperty, '');
|
||||||
|
// parse the returned value into a string...
|
||||||
|
[ ... ]
|
||||||
|
return stringValue;
|
||||||
|
}
|
||||||
|
[ ... ]
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- MyFancyAppInstanceMetadata.js
|
||||||
|
|
||||||
|
class MyFancyAppInstanceMetadata extends InstanceMetadata {
|
||||||
|
// Overriding this method will make all methods implemented in the super class
|
||||||
|
// that rely on it to be immediately available...
|
||||||
|
getRawValue(tagOrProperty, defaultValue) {
|
||||||
|
let rawValue;
|
||||||
|
// retrieve raw value from internal data structures...
|
||||||
|
[ ... ]
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- main.js
|
||||||
|
|
||||||
|
[ ... ]
|
||||||
|
let sopInstaceMetadata = new MyFancyAppInstanceMetadata(myInternalData);
|
||||||
|
if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true
|
||||||
|
// this code will be executed...
|
||||||
|
}
|
||||||
|
if (sopInstaceMetadata instanceof InstanceMetadata) { // also true
|
||||||
|
// this code will also be executed...
|
||||||
|
}
|
||||||
|
// The following will also work since the internal "getRawValue" call inside
|
||||||
|
// "getStringValue" method will now be satisfied... (thanks to the override)
|
||||||
|
let patientName = sopInstaceMetadata.getStringValue('PatientName', '');
|
||||||
|
[ ... ]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
_Copyright © 2016 nucleushealth™. All rights reserved_
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { Metadata } from './Metadata';
|
||||||
|
import { InstanceMetadata } from './InstanceMetadata';
|
||||||
|
|
||||||
|
export class SeriesMetadata extends Metadata {
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
super(data);
|
||||||
|
this._seriesInstanceUID = null;
|
||||||
|
this._instances = []; // InstanceMetadata[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SeriesInstanceUID of the current series.
|
||||||
|
*/
|
||||||
|
getSeriesInstanceUID() {
|
||||||
|
return this._seriesInstanceUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append an instance to the current series.
|
||||||
|
* @param {InstanceMetadata} instance The instance to be added to the current series.
|
||||||
|
* @returns {boolean} Returns true on success, false otherwise.
|
||||||
|
*/
|
||||||
|
addInstance(instance) {
|
||||||
|
let result = false;
|
||||||
|
if (instance instanceof InstanceMetadata && this.getInstanceByUID(instance.getSOPInstanceUID()) === void 0) {
|
||||||
|
this._instances.push(instance);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an instance by index.
|
||||||
|
* @param {number} index An integer representing a list index.
|
||||||
|
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
|
||||||
|
*/
|
||||||
|
getInstanceByIndex(index) {
|
||||||
|
let found; // undefined by default...
|
||||||
|
if (Metadata.isValidIndex(index)) {
|
||||||
|
found = this._instances[index];
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an instance by SOPInstanceUID.
|
||||||
|
* @param {string} uid An UID string.
|
||||||
|
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
|
||||||
|
*/
|
||||||
|
getInstanceByUID(uid) {
|
||||||
|
let found; // undefined by default...
|
||||||
|
if (Metadata.isValidUID(uid)) {
|
||||||
|
found = this._instances.find(instance => {
|
||||||
|
return instance.getSOPInstanceUID() === uid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the number of instances within the current series.
|
||||||
|
* @returns {number} The number of instances in the current series.
|
||||||
|
*/
|
||||||
|
getInstanceCount() {
|
||||||
|
return this._instances.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the supplied callback for each instance in the current series passing
|
||||||
|
* two arguments: instance (an InstanceMetadata instance) and index (the integer
|
||||||
|
* index of the instance within the current series)
|
||||||
|
* @param {function} callback The callback function which will be invoked for each instance in the series.
|
||||||
|
* @returns {undefined} Nothing is returned.
|
||||||
|
*/
|
||||||
|
forEachInstance(callback) {
|
||||||
|
if (Metadata.isValidCallback(callback)) {
|
||||||
|
this._instances.forEach((instance, index) => {
|
||||||
|
callback.call(null, instance, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of an instance inside the series.
|
||||||
|
* @param {InstanceMetadata} instance An instance of the SeriesMetadata class.
|
||||||
|
* @returns {number} The index of the instance inside the series or -1 if not found.
|
||||||
|
*/
|
||||||
|
indexOfInstance(instance) {
|
||||||
|
return this._instances.indexOf(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the current series with another one.
|
||||||
|
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
|
||||||
|
* @returns {boolean} Returns true if both instances refer to the same series.
|
||||||
|
*/
|
||||||
|
equals(series) {
|
||||||
|
const self = this;
|
||||||
|
return (
|
||||||
|
series === self ||
|
||||||
|
(
|
||||||
|
series instanceof SeriesMetadata &&
|
||||||
|
series.getSeriesInstanceUID() === self.getSeriesInstanceUID()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
import { Metadata } from './Metadata';
|
||||||
|
import { SeriesMetadata } from './SeriesMetadata';
|
||||||
|
|
||||||
|
export class StudyMetadata extends Metadata {
|
||||||
|
constructor(data) {
|
||||||
|
super(data);
|
||||||
|
this._studyInstanceUID = null;
|
||||||
|
this._series = []; // SeriesMetadata[]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the StudyInstanceUID of the current study.
|
||||||
|
*/
|
||||||
|
getStudyInstanceUID() {
|
||||||
|
return this._studyInstanceUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a series to the current study.
|
||||||
|
* @param {SeriesMetadata} series The series to be added to the current study.
|
||||||
|
* @returns {boolean} Returns true on success, false otherwise.
|
||||||
|
*/
|
||||||
|
addSeries(series) {
|
||||||
|
let result = false;
|
||||||
|
if (series instanceof SeriesMetadata && this.getSeriesByUID(series.getSeriesInstanceUID()) === void 0) {
|
||||||
|
this._series.push(series);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a series by index.
|
||||||
|
* @param {number} index An integer representing a list index.
|
||||||
|
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
|
||||||
|
*/
|
||||||
|
getSeriesByIndex(index) {
|
||||||
|
let found; // undefined by default...
|
||||||
|
if (Metadata.isValidIndex(index)) {
|
||||||
|
found = this._series[index];
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a series by SeriesInstanceUID.
|
||||||
|
* @param {string} uid An UID string.
|
||||||
|
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
|
||||||
|
*/
|
||||||
|
getSeriesByUID(uid) {
|
||||||
|
let found; // undefined by default...
|
||||||
|
if (Metadata.isValidUID(uid)) {
|
||||||
|
found = this._series.find(series => {
|
||||||
|
return series.getSeriesInstanceUID() === uid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the number of series within the current study.
|
||||||
|
* @returns {number} The number of series in the current study.
|
||||||
|
*/
|
||||||
|
getSeriesCount() {
|
||||||
|
return this._series.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the number of instances within the current study.
|
||||||
|
* @returns {number} The number of instances in the current study.
|
||||||
|
*/
|
||||||
|
getInstanceCount() {
|
||||||
|
return this._series.reduce((sum, series) => {
|
||||||
|
return sum + series.getInstanceCount();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the supplied callback for each series in the current study passing
|
||||||
|
* two arguments: series (a SeriesMetadata instance) and index (the integer
|
||||||
|
* index of the series within the current study)
|
||||||
|
* @param {function} callback The callback function which will be invoked for each series instance.
|
||||||
|
* @returns {undefined} Nothing is returned.
|
||||||
|
*/
|
||||||
|
forEachSeries(callback) {
|
||||||
|
if (Metadata.isValidCallback(callback)) {
|
||||||
|
this._series.forEach((series, index) => {
|
||||||
|
callback.call(null, series, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of a series inside the study.
|
||||||
|
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
|
||||||
|
* @returns {number} The index of the series inside the study or -1 if not found.
|
||||||
|
*/
|
||||||
|
indexOfSeries(series) {
|
||||||
|
return this._series.indexOf(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the current study instance with another one.
|
||||||
|
* @param {StudyMetadata} study An instance of the StudyMetadata class.
|
||||||
|
* @returns {boolean} Returns true if both instances refer to the same study.
|
||||||
|
*/
|
||||||
|
equals(study) {
|
||||||
|
const self = this;
|
||||||
|
return (
|
||||||
|
study === self ||
|
||||||
|
(
|
||||||
|
study instanceof StudyMetadata &&
|
||||||
|
study.getStudyInstanceUID() === self.getStudyInstanceUID()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first instance of the first series
|
||||||
|
* @return {InstanceMetadata} InstanceMetadata class object or undefined if it doenst exist
|
||||||
|
*/
|
||||||
|
getFirstInstance() {
|
||||||
|
let firstInstance;
|
||||||
|
const firstSeries = this.getSeriesByIndex(0);
|
||||||
|
|
||||||
|
if (firstSeries) {
|
||||||
|
firstInstance = firstSeries.getInstanceByIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns a string describing an image orientation
|
||||||
|
* (e.g. axial, sagittal, coronal)
|
||||||
|
* If no classification is determined, it returns undefined
|
||||||
|
*
|
||||||
|
* @param {string} [imageId]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function classifyImageOrientation(imageId) {
|
||||||
|
// First we check if this imageId has already been classified, so we can return
|
||||||
|
// the classification from the cache
|
||||||
|
if (OHIF.viewer.linkSeries.classifiedOrientation.hasOwnProperty(imageId)) {
|
||||||
|
return OHIF.viewer.linkSeries.classifiedOrientation[imageId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the image plane normal
|
||||||
|
const imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
|
||||||
|
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imageNormal = imagePlane.rowCosines.clone().cross(imagePlane.columnCosines);
|
||||||
|
|
||||||
|
// These represent the normal vectors to three standard image planes
|
||||||
|
const normals = {
|
||||||
|
axial: new cornerstoneMath.Vector3(0, 0, 1),
|
||||||
|
coronal: new cornerstoneMath.Vector3(0, 1, 0),
|
||||||
|
sagittal: new cornerstoneMath.Vector3(1, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
const PI = Math.PI;
|
||||||
|
// This loop checks the angle between the current image plane normal
|
||||||
|
// and that of the three standard image planes above. If the two
|
||||||
|
// normal vectors are within 15 degrees of each other, the current image
|
||||||
|
// is classified as the relevant standard image plane (e.g. axial).
|
||||||
|
let classifiedOrientation;
|
||||||
|
Object.keys(normals).some(orientation => {
|
||||||
|
let angleInRadians = imageNormal.angleTo(normals[orientation]);
|
||||||
|
|
||||||
|
angleInRadians = Math.abs(angleInRadians);
|
||||||
|
if (angleInRadians < PI / 12 || angleInRadians === PI) { // Pi / 12 radians = 15 degrees
|
||||||
|
classifiedOrientation = orientation;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no classification was determined, stop here and dump a warning to the console
|
||||||
|
if (!classifiedOrientation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, update the cache with the classified orientation for this imageId
|
||||||
|
OHIF.viewer.linkSeries.classifiedOrientation[imageId] = classifiedOrientation;
|
||||||
|
|
||||||
|
return classifiedOrientation;
|
||||||
|
};
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export const crosshairsSynchronizers = {
|
||||||
|
synchronizers: {}
|
||||||
|
};
|
||||||
72
Packages/ohif-viewerbase/client/lib/dialogUtils.js
Normal file
72
Packages/ohif-viewerbase/client/lib/dialogUtils.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Blaze } from 'meteor/blaze';
|
||||||
|
import { Template } from 'meteor/templating';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
|
||||||
|
import { setFocusToActiveViewport } from './setFocusToActiveViewport';
|
||||||
|
|
||||||
|
let doneCallbackFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the backdrop abd closes opened dialog
|
||||||
|
* and focus to the active viewport. If a done callback is set,
|
||||||
|
* it's called before
|
||||||
|
* @param {Boolean} runCallback Indicate if callback function needs to be called. Default: true
|
||||||
|
*/
|
||||||
|
const closeHandler = (runCallback = true) => {
|
||||||
|
// Check if callback function exists
|
||||||
|
if (runCallback && typeof doneCallbackFunction === 'function') {
|
||||||
|
doneCallbackFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the lesion dialog
|
||||||
|
$('#confirmDeleteDialog').css('display', 'none');
|
||||||
|
|
||||||
|
// Remove the backdrop
|
||||||
|
$('.removableBackdrop').remove();
|
||||||
|
|
||||||
|
// Remove the callback
|
||||||
|
doneCallbackFunction = undefined;
|
||||||
|
|
||||||
|
// Restore the focus to the active viewport
|
||||||
|
setFocusToActiveViewport();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the confirmation dialog template and the removable backdrop element
|
||||||
|
*
|
||||||
|
* @param doneCallback A callback
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
const showConfirmDialog = (doneCallback, options) => {
|
||||||
|
// Show the backdrop
|
||||||
|
options = options || {};
|
||||||
|
Blaze.renderWithData(Template.removableBackdrop, options, document.body);
|
||||||
|
|
||||||
|
let confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||||
|
confirmDeleteDialog.remove();
|
||||||
|
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
Blaze.renderWithData(Template.confirmDeleteDialog, options, viewer);
|
||||||
|
|
||||||
|
// Make sure the context menu is closed when the user clicks away
|
||||||
|
$('.removableBackdrop').one('mousedown touchstart', () => {
|
||||||
|
// Close dialog without calling callback
|
||||||
|
closeHandler(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||||
|
confirmDeleteDialog.css('display', 'block');
|
||||||
|
confirmDeleteDialog.focus();
|
||||||
|
|
||||||
|
// If callback function is defined, save it for closeHandler
|
||||||
|
if (doneCallback && typeof doneCallback === 'function') {
|
||||||
|
doneCallbackFunction = doneCallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogUtils = {
|
||||||
|
showConfirmDialog,
|
||||||
|
closeHandler
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dialogUtils };
|
||||||
27
Packages/ohif-viewerbase/client/lib/getElementIfNotEmpty.js
Normal file
27
Packages/ohif-viewerbase/client/lib/getElementIfNotEmpty.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Helper function to quickly obtain the frameOfReferenceUID
|
||||||
|
* for a given element from the enabled image's metadata.
|
||||||
|
*
|
||||||
|
* If no image, imagePlane, or frameOfReferenceUID is available,
|
||||||
|
* the function will return undefined.
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFrameOfReferenceUID(element) {
|
||||||
|
var enabledElement;
|
||||||
|
try {
|
||||||
|
enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
} catch(error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabledElement || !enabledElement.image) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageId = enabledElement.image.imageId;
|
||||||
|
var imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
|
||||||
|
if (!imagePlane || !imagePlane.frameOfReferenceUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return imagePlane.frameOfReferenceUID;
|
||||||
|
}
|
||||||
251
Packages/ohif-viewerbase/client/lib/getSortedData.js
Normal file
251
Packages/ohif-viewerbase/client/lib/getSortedData.js
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { ImageSet } from './classes/ImageSet';
|
||||||
|
|
||||||
|
const sortBySeriesNumberAndAcquisition = (a, b) => {
|
||||||
|
if (a.seriesNumber && b.seriesNumber) {
|
||||||
|
if (a.seriesNumber !== b.seriesNumber) {
|
||||||
|
return a.seriesNumber - b.seriesNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.acquisitionDateTime && b.acquisitionDateTime) {
|
||||||
|
return a.acquisitionDateTime.localeCompare(b.acquisitionDateTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeImageSet = sopInstances => {
|
||||||
|
const imageSet = new ImageSet(sopInstances);
|
||||||
|
const sopInstance = sopInstances[0];
|
||||||
|
|
||||||
|
// list of attributes to be appended to ImageSet instance...
|
||||||
|
const attributes = {
|
||||||
|
seriesInstanceUid: sopInstance.getRawValue('x0020000e'),
|
||||||
|
seriesNumber: sopInstance.getIntValue('x00200011'),
|
||||||
|
seriesDescription: sopInstance.getRawValue('x0008103e', ''),
|
||||||
|
acquisitionDateTime: sopInstance.getRawValue('x00080022', '') + sopInstance.getRawValue('x00080032', ''),
|
||||||
|
numImageFrames: sopInstances.length,
|
||||||
|
frameRate: sopInstance.getFloatValue('x00181063', 0, 0),
|
||||||
|
studyInstanceUid: sopInstance.getRawValue('x0020000d'),
|
||||||
|
displaySetInstanceUid: imageSet.uid // create an alias for the imageSet UID
|
||||||
|
};
|
||||||
|
|
||||||
|
const frameTime = sopInstance.getFloatValue('x00181063', 0, 0);
|
||||||
|
const recommendedDisplayFrameRate = sopInstance.getFloatValue('x00082144', 0, 0);
|
||||||
|
if (frameTime) {
|
||||||
|
// FrameTime is in milliseconds, so we use 1000/frameTime to calculate
|
||||||
|
// the number of frames per second.
|
||||||
|
attributes.frameRate = 1000 / frameTime;
|
||||||
|
attributes.isClip = true;
|
||||||
|
} else if (recommendedDisplayFrameRate) {
|
||||||
|
// Recommended Display FrameRate is specified in frames per second as an integer
|
||||||
|
attributes.frameRate = recommendedDisplayFrameRate;
|
||||||
|
attributes.isClip = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageSet.setAttributes(attributes);
|
||||||
|
|
||||||
|
// Sort the images in this series
|
||||||
|
imageSet.sortBy( (a, b) => {
|
||||||
|
const aInstanceNumber = a.getFloatValue('x00200013');
|
||||||
|
const bInstanceNumber = b.getFloatValue('x00200013');
|
||||||
|
|
||||||
|
if (aInstanceNumber && bInstanceNumber) {
|
||||||
|
if (aInstanceNumber !== bInstanceNumber) {
|
||||||
|
return aInstanceNumber - bInstanceNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aAcquisitionNumber = a.getFloatValue('x00200012');
|
||||||
|
const bAcquisitionNumber = b.getFloatValue('x00200012');
|
||||||
|
|
||||||
|
if (aAcquisitionNumber && bAcquisitionNumber) {
|
||||||
|
if (aAcquisitionNumber !== bAcquisitionNumber) {
|
||||||
|
return aAcquisitionNumber - bAcquisitionNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aAcquisitionDateTime = a.getRawValue('x00080022', '') + a.getRawValue('x00080032', '');
|
||||||
|
const bAcquisitionDateTime = b.getRawValue('x00080022', '') + b.getRawValue('x00080032', '');
|
||||||
|
|
||||||
|
if (aAcquisitionDateTime && bAcquisitionDateTime) {
|
||||||
|
return aAcquisitionDateTime.localeCompare(bAcquisitionDateTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return imageSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMultiFrame = instanceView => {
|
||||||
|
const numFrames = instanceView.getIntValue('x00280008');
|
||||||
|
return (numFrames > 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSingleImageModality = modality => {
|
||||||
|
const singleImageModalities = ['CR', 'MG', 'DX', 'RG', 'PX', 'XA', 'XR']
|
||||||
|
return singleImageModalities.indexOf(modality) > -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const seriesSplittingRules = [
|
||||||
|
{
|
||||||
|
tag: 'x00180010', // ContrastBolusAgent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'x00180086', // Echo Number
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Some other potential examples of tags to split on --- //
|
||||||
|
/*{
|
||||||
|
tag: 'x00180020', // Scanning Sequence
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'x00180050', // Slice Thickness
|
||||||
|
},*/
|
||||||
|
/*{
|
||||||
|
tag: 'x00180081', // Echo Time
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'x00181210', // Convolution Kernel
|
||||||
|
},*/
|
||||||
|
/*{
|
||||||
|
tag: 'x00200012', // Acquisition Number
|
||||||
|
}*/
|
||||||
|
];
|
||||||
|
|
||||||
|
const stackOfInstancesToDisplaySets = (sopInstances, seriesView) => {
|
||||||
|
// If no instances are provided, return an empty array
|
||||||
|
if (!sopInstances.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an empty array to store our display sets in
|
||||||
|
const displaySets = [];
|
||||||
|
|
||||||
|
// Use Underscore's groupBy function to group our sopInstances
|
||||||
|
// by a key. In our case, the key is a combined string which represents
|
||||||
|
// the value of the DICOM tags listed in the seriesSplittingRules array.
|
||||||
|
// Currently this uses tags for EchoNumber and ContrastBolusAgent.
|
||||||
|
const groups = _.groupBy(sopInstances, sopInstance => {
|
||||||
|
// Create an empty string to store our key
|
||||||
|
let result = '';
|
||||||
|
const instanceView = sopInstance.getData();
|
||||||
|
|
||||||
|
// For each rule, find out if the tag exists
|
||||||
|
seriesSplittingRules.forEach(rule => {
|
||||||
|
// Include a delimitation character between each tag value
|
||||||
|
result += ';'
|
||||||
|
|
||||||
|
// If the tag exists, concatenate its value to our key
|
||||||
|
if (instanceView.exists(rule.tag)) {
|
||||||
|
result += instanceView.raw(rule.tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the key to be used for grouping.
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
|
||||||
|
const seriesInstanceCount = seriesView.getInstanceCount();
|
||||||
|
|
||||||
|
Object.keys(groups).forEach(groupKey => {
|
||||||
|
// For each group, obtain the instances in the stack
|
||||||
|
const stackInstances = groups[groupKey];
|
||||||
|
|
||||||
|
// Create a new display set from the images in this stack
|
||||||
|
const displaySet = makeImageSet(stackInstances);
|
||||||
|
displaySet.setAttribute('seriesInstanceCount', seriesInstanceCount);
|
||||||
|
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
});
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function extracts and produces sorted lists of image sets and non-viewable instance
|
||||||
|
* sets from a studyMetadata {StudyMetadata instance}.
|
||||||
|
*
|
||||||
|
* If an optional series number is specified, only the image set with this series number
|
||||||
|
* will be returned (as the sole element in an array).
|
||||||
|
*
|
||||||
|
* The function returns an object with two fields, imageSets and nonViewable, both of which
|
||||||
|
* are arrays. imageSets is an array of sorted image sets and nonViewable is an array of
|
||||||
|
* non-viewable sopInstances.
|
||||||
|
*
|
||||||
|
* @param studyMetadata
|
||||||
|
* @param {number} [seriesNumber] One series number. If specified, the function only returns the imageSet with this seriesNumber
|
||||||
|
*
|
||||||
|
* @returns {{imageSets: Array, nonViewable: Array}}
|
||||||
|
*/
|
||||||
|
getSortedData = function(studyMetadata, seriesNumber) {
|
||||||
|
let imageSets = [];
|
||||||
|
let nonViewable = [];
|
||||||
|
|
||||||
|
studyMetadata.forEachSeries((series, seriesNum) => {
|
||||||
|
const firstInstance = series.getInstanceByIndex(0);
|
||||||
|
|
||||||
|
// If a specific series number is given, skip all series except this one
|
||||||
|
if (seriesNumber !== undefined && firstInstance.getIntValue('x00200011', '') !== seriesNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sopInstances = [];
|
||||||
|
let imageSet;
|
||||||
|
|
||||||
|
const seriesInstanceCount = series.getInstanceCount();
|
||||||
|
|
||||||
|
series.forEachInstance(instanceMetadata => {
|
||||||
|
// Skip non image types (an image must have the 'rows' tag)
|
||||||
|
const rows = instanceMetadata.tagExists('x00280010');
|
||||||
|
|
||||||
|
if (!rows) {
|
||||||
|
nonViewable.push(instanceMetadata);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modality = firstInstance.getRawValue('x00080060', '');
|
||||||
|
const singleImageModality = isSingleImageModality(modality);
|
||||||
|
|
||||||
|
// If we detect a multi-frame instance
|
||||||
|
if (isMultiFrame(instanceMetadata)) {
|
||||||
|
// First, sort all sopInstances currently in our stack into display sets using
|
||||||
|
// our series splitting rules
|
||||||
|
const displaySets = stackOfInstancesToDisplaySets(sopInstances, series);
|
||||||
|
imageSets = imageSets.concat(displaySets);
|
||||||
|
|
||||||
|
// Next, make another display set for the multi-frame instance
|
||||||
|
imageSet = makeImageSet([ instanceMetadata ]);
|
||||||
|
imageSet.setAttributes({
|
||||||
|
seriesInstanceCount: seriesInstanceCount,
|
||||||
|
numImageFrames: instanceMetadata.getIntValue('x00280008', 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
imageSets.push(imageSet);
|
||||||
|
|
||||||
|
// Reset our stack of sopInstances
|
||||||
|
sopInstances = [];
|
||||||
|
} else if (singleImageModality) {
|
||||||
|
imageSet = makeImageSet([ instanceMetadata ]);
|
||||||
|
imageSet.setAttribute('seriesInstanceCount', seriesInstanceCount);
|
||||||
|
imageSets.push(imageSet);
|
||||||
|
} else {
|
||||||
|
// If no multi-frame instances have been discovered
|
||||||
|
sopInstances.push(instanceMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create display sets from any remaining sopInstances in the series
|
||||||
|
const displaySets = stackOfInstancesToDisplaySets(sopInstances, series);
|
||||||
|
imageSets = imageSets.concat(displaySets);
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort imageSets
|
||||||
|
imageSets.sort(sortBySeriesNumberAndAcquisition);
|
||||||
|
nonViewable.sort(sortBySeriesNumberAndAcquisition);
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageSets,
|
||||||
|
nonViewable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getSortedData };
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { getElementIfNotEmpty } from './getElementIfNotEmpty.js';
|
||||||
|
|
||||||
|
export function getStackDataIfNotEmpty(viewportIndex) {
|
||||||
|
const element = getElementIfNotEmpty(viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackToolData = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
if (!stackToolData ||
|
||||||
|
!stackToolData.data ||
|
||||||
|
!stackToolData.data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = stackToolData.data[0];
|
||||||
|
if (!stack) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
39
Packages/ohif-viewerbase/client/lib/helpers/index.js
Normal file
39
Packages/ohif-viewerbase/client/lib/helpers/index.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Helpers with exposed symbols...
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isTouchDevice } from './isTouchDevice';
|
||||||
|
import { formatPN } from './formatPN';
|
||||||
|
import { formatDA } from './formatDA';
|
||||||
|
import { formatTM } from './formatTM';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers with side effects only...
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './formatJSDate';
|
||||||
|
import './jsDateFromNow';
|
||||||
|
import './formatNumberPrecision';
|
||||||
|
import './inc';
|
||||||
|
import './isDisplaySetActive';
|
||||||
|
import './getUsername';
|
||||||
|
import './capitalizeFirstLetter';
|
||||||
|
import './objectToPairs';
|
||||||
|
import './objectEach';
|
||||||
|
import './ifTypeIs';
|
||||||
|
import './prettyPrintStringify';
|
||||||
|
import './sorting';
|
||||||
|
import './studyThumbnails';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed interface...
|
||||||
|
*/
|
||||||
|
|
||||||
|
const helpers = {
|
||||||
|
isTouchDevice,
|
||||||
|
formatPN,
|
||||||
|
formatDA,
|
||||||
|
formatTM
|
||||||
|
};
|
||||||
|
|
||||||
|
export { helpers };
|
||||||
375
Packages/ohif-viewerbase/client/lib/hotkeyUtils.js
Normal file
375
Packages/ohif-viewerbase/client/lib/hotkeyUtils.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
import { toolManager } from './toolManager';
|
||||||
|
import { setActiveViewport } from './setActiveViewport';
|
||||||
|
import { switchToImageRelative } from './switchToImageRelative';
|
||||||
|
import { switchToImageByIndex } from './switchToImageByIndex';
|
||||||
|
import { viewportUtils } from './viewportUtils';
|
||||||
|
import { panelNavigation } from './panelNavigation';
|
||||||
|
import { seriesNavigation } from './seriesNavigation';
|
||||||
|
import { WLPresets } from './WLPresets';
|
||||||
|
|
||||||
|
// TODO: add this to namespace definitions
|
||||||
|
Meteor.startup(function() {
|
||||||
|
if (!OHIF.viewer) {
|
||||||
|
OHIF.viewer = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
OHIF.viewer.loadIndicatorDelay = 200;
|
||||||
|
OHIF.viewer.defaultTool = 'wwwc';
|
||||||
|
OHIF.viewer.refLinesEnabled = true;
|
||||||
|
OHIF.viewer.isPlaying = {};
|
||||||
|
OHIF.viewer.cine = {
|
||||||
|
framesPerSecond: 24,
|
||||||
|
loop: true
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIF.viewer.defaultHotkeys = {
|
||||||
|
defaultTool: 'ESC',
|
||||||
|
angle: 'A',
|
||||||
|
stackScroll: 'S',
|
||||||
|
pan: 'P',
|
||||||
|
magnify: 'M',
|
||||||
|
scrollDown: 'DOWN',
|
||||||
|
scrollUp: 'UP',
|
||||||
|
nextDisplaySet: 'PAGEDOWN',
|
||||||
|
previousDisplaySet: 'PAGEUP',
|
||||||
|
nextPanel: 'RIGHT',
|
||||||
|
previousPanel: 'LEFT',
|
||||||
|
invert: 'I',
|
||||||
|
flipV: 'V',
|
||||||
|
flipH: 'H',
|
||||||
|
wwwc: 'W',
|
||||||
|
zoom: 'Z',
|
||||||
|
cinePlay: 'SPACE',
|
||||||
|
rotateR: 'R',
|
||||||
|
rotateL: 'L',
|
||||||
|
toggleOverlayTags: 'SHIFT',
|
||||||
|
WLPresetSoftTissue: ['NUMPAD1', '1'],
|
||||||
|
WLPresetLung: ['NUMPAD2', '2'],
|
||||||
|
WLPresetLiver: ['NUMPAD3', '3'],
|
||||||
|
WLPresetBone: ['NUMPAD4', '4'],
|
||||||
|
WLPresetBrain: ['NUMPAD5', '5']
|
||||||
|
};
|
||||||
|
|
||||||
|
// For now
|
||||||
|
OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys;
|
||||||
|
|
||||||
|
OHIF.viewer.hotkeyFunctions = {
|
||||||
|
wwwc() {
|
||||||
|
toolManager.setActiveTool('wwwc');
|
||||||
|
},
|
||||||
|
zoom() {
|
||||||
|
toolManager.setActiveTool('zoom');
|
||||||
|
},
|
||||||
|
angle() {
|
||||||
|
toolManager.setActiveTool('angle');
|
||||||
|
},
|
||||||
|
dragProbe() {
|
||||||
|
toolManager.setActiveTool('dragProbe');
|
||||||
|
},
|
||||||
|
ellipticalRoi() {
|
||||||
|
toolManager.setActiveTool('ellipticalRoi');
|
||||||
|
},
|
||||||
|
magnify() {
|
||||||
|
toolManager.setActiveTool('magnify');
|
||||||
|
},
|
||||||
|
annotate() {
|
||||||
|
toolManager.setActiveTool('annotate');
|
||||||
|
},
|
||||||
|
stackScroll() {
|
||||||
|
toolManager.setActiveTool('stackScroll');
|
||||||
|
},
|
||||||
|
pan() {
|
||||||
|
toolManager.setActiveTool('pan');
|
||||||
|
},
|
||||||
|
length() {
|
||||||
|
toolManager.setActiveTool('length');
|
||||||
|
},
|
||||||
|
spine() {
|
||||||
|
toolManager.setActiveTool('spine');
|
||||||
|
},
|
||||||
|
wwwcRegion() {
|
||||||
|
toolManager.setActiveTool('wwwcRegion');
|
||||||
|
},
|
||||||
|
zoomIn() {
|
||||||
|
const button = document.getElementById('zoomIn');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.zoomIn();
|
||||||
|
},
|
||||||
|
zoomOut() {
|
||||||
|
const button = document.getElementById('zoomOut');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.zoomOut();
|
||||||
|
},
|
||||||
|
zoomToFit() {
|
||||||
|
const button = document.getElementById('zoomToFit');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.zoomToFit();
|
||||||
|
},
|
||||||
|
scrollDown() {
|
||||||
|
const container = $('.viewportContainer.active');
|
||||||
|
const button = container.find('#nextImage').get(0);
|
||||||
|
|
||||||
|
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||||
|
flashButton(button);
|
||||||
|
switchToImageRelative(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollFirstImage() {
|
||||||
|
const container = $('.viewportContainer.active');
|
||||||
|
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||||
|
switchToImageByIndex(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollLastImage() {
|
||||||
|
const container = $('.viewportContainer.active');
|
||||||
|
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||||
|
switchToImageByIndex(-1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollUp() {
|
||||||
|
const container = $('.viewportContainer.active');
|
||||||
|
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||||
|
const button = container.find('#prevImage').get(0);
|
||||||
|
flashButton(button);
|
||||||
|
switchToImageRelative(-1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousDisplaySet() {
|
||||||
|
OHIF.viewer.moveDisplaySets(false);
|
||||||
|
},
|
||||||
|
nextDisplaySet() {
|
||||||
|
OHIF.viewer.moveDisplaySets(true);
|
||||||
|
},
|
||||||
|
nextPanel() {
|
||||||
|
panelNavigation.loadNextActivePanel();
|
||||||
|
},
|
||||||
|
previousPanel() {
|
||||||
|
panelNavigation.loadPreviousActivePanel();
|
||||||
|
},
|
||||||
|
invert() {
|
||||||
|
const button = document.getElementById('invert');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.invert();
|
||||||
|
},
|
||||||
|
flipV() {
|
||||||
|
const button = document.getElementById('flipV');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.flipV();
|
||||||
|
},
|
||||||
|
flipH() {
|
||||||
|
const button = document.getElementById('flipH');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.flipH();
|
||||||
|
},
|
||||||
|
rotateR() {
|
||||||
|
const button = document.getElementById('rotateR');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.rotateR();
|
||||||
|
},
|
||||||
|
rotateL() {
|
||||||
|
const button = document.getElementById('rotateL');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.rotateL();
|
||||||
|
},
|
||||||
|
cinePlay() {
|
||||||
|
viewportUtils.toggleCinePlay();
|
||||||
|
},
|
||||||
|
defaultTool() {
|
||||||
|
const tool = toolManager.getDefaultTool();
|
||||||
|
toolManager.setActiveTool(tool);
|
||||||
|
},
|
||||||
|
toggleOverlayTags() {
|
||||||
|
const dicomTags = $('.imageViewerViewportOverlay .dicomTag');
|
||||||
|
if (dicomTags.eq(0).css('display') === 'none') {
|
||||||
|
dicomTags.show();
|
||||||
|
} else {
|
||||||
|
dicomTags.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetStack() {
|
||||||
|
const button = document.getElementById('resetStack');
|
||||||
|
flashButton(button);
|
||||||
|
resetStack();
|
||||||
|
},
|
||||||
|
clearImageAnnotations() {
|
||||||
|
const button = document.getElementById('clearImageAnnotations');
|
||||||
|
flashButton(button);
|
||||||
|
clearImageAnnotations();
|
||||||
|
},
|
||||||
|
nextSeries () {
|
||||||
|
seriesNavigation.loadNextSeries();
|
||||||
|
},
|
||||||
|
previousSeries () {
|
||||||
|
seriesNavigation.loadPreviousSeries();
|
||||||
|
},
|
||||||
|
cineDialog () {
|
||||||
|
/**
|
||||||
|
* TODO: This won't work in OHIF's, since this element
|
||||||
|
* doesn't exist
|
||||||
|
*/
|
||||||
|
const button = document.getElementById('cine');
|
||||||
|
flashButton(button);
|
||||||
|
viewportUtils.toggleCineDialog();
|
||||||
|
button.classList.toggle('active');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIF.viewer.loadedSeriesData = {};
|
||||||
|
OHIF.viewer.linkSeries = {
|
||||||
|
classifiedOrientation: {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define a jQuery reverse function
|
||||||
|
$.fn.reverse = [].reverse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides OHIF's refLinesEnabled
|
||||||
|
* @param {Boolean} refLinesEnabled True to enable and False to disable
|
||||||
|
*/
|
||||||
|
function setOHIFRefLines(refLinesEnabled) {
|
||||||
|
OHIF.viewer.refLinesEnabled = refLinesEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides OHIF's hotkeys
|
||||||
|
* @param {Object} hotkeys Object with hotkeys mapping
|
||||||
|
*/
|
||||||
|
function setOHIFHotkeys(hotkeys) {
|
||||||
|
OHIF.viewer.hotkeys = hotkeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global function to merge different hotkeys configurations
|
||||||
|
* but avoiding conflicts between different keys with same action
|
||||||
|
* When this occurs, it will delete the action from OHIF's configuration
|
||||||
|
* So if you want to keep all OHIF's actions, use an unused-ohif-key
|
||||||
|
* Used for compatibility with others systems only
|
||||||
|
*
|
||||||
|
* @param hotkeysActions {object} Object with actions map
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
function mergeHotkeys(hotkeysActions) {
|
||||||
|
// Merge hotkeys, overriding OHIF's settings
|
||||||
|
let mergedHotkeys = {
|
||||||
|
...OHIF.viewer.defaultHotkeys,
|
||||||
|
...hotkeysActions
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultHotkeys = OHIF.viewer.defaultHotkeys;
|
||||||
|
const hotkeysKeys = Object.keys(hotkeysActions);
|
||||||
|
|
||||||
|
// Check for conflicts with same keys but different actions
|
||||||
|
Object.keys(defaultHotkeys).forEach(ohifAction => {
|
||||||
|
hotkeysKeys.forEach(definedAction => {
|
||||||
|
// Different action but same key:
|
||||||
|
// Remove action from merge if is not in "hotkeysActions"
|
||||||
|
// If it is, it's already merged so nothing to do
|
||||||
|
if(ohifAction !== definedAction && hotkeysActions[definedAction] === defaultHotkeys[ohifAction] && !hotkeysActions[ohifAction]) {
|
||||||
|
delete mergedHotkeys[ohifAction];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedHotkeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an active class to a button for 100ms only
|
||||||
|
* to give the impressiont the button was pressed.
|
||||||
|
* This is for tools that don't keep the button "pressed"
|
||||||
|
* all the time the tool is active.
|
||||||
|
*
|
||||||
|
* @param button DOM Element for the button to be "flashed"
|
||||||
|
*/
|
||||||
|
function flashButton(button) {
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.classList.add('active');
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('active');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a task to a hotkey keydown event
|
||||||
|
* @param {String} hotkey keyboard key
|
||||||
|
* @param {String} task task function name
|
||||||
|
*/
|
||||||
|
function bindHotkey(hotkey, task) {
|
||||||
|
var hotkeyFunctions = OHIF.viewer.hotkeyFunctions;
|
||||||
|
|
||||||
|
// Only bind defined, non-empty HotKeys
|
||||||
|
if (!hotkey || hotkey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fn;
|
||||||
|
if (task.indexOf('WLPreset') > -1) {
|
||||||
|
var presetName = task.replace('WLPreset', '');
|
||||||
|
fn = function() {
|
||||||
|
WLPresets.applyWLPresetToActiveElement(presetName);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
fn = hotkeyFunctions[task];
|
||||||
|
|
||||||
|
// If the function doesn't exist in the
|
||||||
|
// hotkey function list, try the viewer-specific function list
|
||||||
|
if (!fn && OHIF.viewer && OHIF.viewer.functionList) {
|
||||||
|
fn = OHIF.viewer.functionList[task];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hotKeyForBinding = hotkey.toLowerCase();
|
||||||
|
|
||||||
|
$(document).bind('keydown', hotKeyForBinding, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds all hotkeys keydown events to the tasks defined in
|
||||||
|
* OHIF.viewer.hotkeys or a given param
|
||||||
|
* @param {Object} hotkeys hotkey and task mapping (not required). If not given, uses OHIF.viewer.hotkeys
|
||||||
|
*/
|
||||||
|
function enableHotkeys(hotkeys) {
|
||||||
|
const viewerHotkeys = hotkeys || OHIF.viewer.hotkeys;
|
||||||
|
|
||||||
|
$(document).unbind('keydown');
|
||||||
|
|
||||||
|
Object.keys(viewerHotkeys).forEach(function(task) {
|
||||||
|
const taskHotkeys = viewerHotkeys[task];
|
||||||
|
if (!taskHotkeys || !taskHotkeys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskHotkeys instanceof Array) {
|
||||||
|
taskHotkeys.forEach(function(hotkey) {
|
||||||
|
bindHotkey(hotkey, task);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// taskHotkeys represents a single key
|
||||||
|
bindHotkey(taskHotkeys, task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export functions inside hotkeyUtils namespace.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hotkeyUtils = {
|
||||||
|
setOHIFRefLines, /* @TODO: find a better place for this... */
|
||||||
|
setOHIFHotkeys,
|
||||||
|
mergeHotkeys,
|
||||||
|
enableHotkeys
|
||||||
|
};
|
||||||
|
|
||||||
|
export { hotkeyUtils };
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
export const imageViewerViewportData = {
|
||||||
|
callbacks: {},
|
||||||
|
extendData() {
|
||||||
|
// No-Op function...
|
||||||
|
}
|
||||||
|
};
|
||||||
53
Packages/ohif-viewerbase/client/lib/panelNavigation.js
Normal file
53
Packages/ohif-viewerbase/client/lib/panelNavigation.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Session } from 'meteor/session';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
import { setActiveViewport } from './setActiveViewport';
|
||||||
|
|
||||||
|
const loadPreviousActivePanel = () => {
|
||||||
|
OHIF.log.info('nextActivePanel');
|
||||||
|
let currentIndex = Session.get('activeViewport');
|
||||||
|
currentIndex--;
|
||||||
|
|
||||||
|
const viewports = $('.imageViewerViewport');
|
||||||
|
const numViewports = viewports.length;
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
currentIndex = numViewports - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = viewports.get(currentIndex);
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveViewport(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadNextActivePanel = () => {
|
||||||
|
OHIF.log.info('nextActivePanel');
|
||||||
|
let currentIndex = Session.get('activeViewport');
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
const viewports = $('.imageViewerViewport');
|
||||||
|
const numViewports = viewports.length;
|
||||||
|
if (currentIndex >= numViewports) {
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = viewports.get(currentIndex);
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveViewport(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export functions inside panelNavigation namespace.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const panelNavigation = {
|
||||||
|
loadPreviousActivePanel,
|
||||||
|
loadNextActivePanel
|
||||||
|
};
|
||||||
|
|
||||||
|
export { panelNavigation };
|
||||||
91
Packages/ohif-viewerbase/client/lib/seriesNavigation.js
Normal file
91
Packages/ohif-viewerbase/client/lib/seriesNavigation.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Session } from 'meteor/session';
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
// @TODO: import symbols into local scope: Studies and ViewerData
|
||||||
|
|
||||||
|
const getStudyFromStudyInstanceUid = studyInstanceUid => {
|
||||||
|
// @TypeSafeStudies
|
||||||
|
return Studies.findBy({ studyInstanceUid });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNumberOfStacks = studyInstanceUid => {
|
||||||
|
// May not work if the Study contains non-image series?
|
||||||
|
const study = getStudyFromStudyInstanceUid(studyInstanceUid);
|
||||||
|
|
||||||
|
if (!study || !study.series || !study.series.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return study.series.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveViewportIndex = () => {
|
||||||
|
return Session.get('activeViewport');
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSeries = indexCalculator => {
|
||||||
|
if(typeof indexCalculator !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportIndex = getActiveViewportIndex();
|
||||||
|
const contentId = Session.get('activeContentId');
|
||||||
|
const viewerData = ViewerData[contentId].loadedSeriesData[viewportIndex];
|
||||||
|
|
||||||
|
if(!viewerData && !viewerData.studyInstanceUid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { studyInstanceUid, seriesInstanceUid, displaySetInstanceUid } = viewerData;
|
||||||
|
|
||||||
|
if(!studyInstanceUid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfStacks = getNumberOfStacks(studyInstanceUid);
|
||||||
|
if (!numberOfStacks || numberOfStacks === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = getStudyFromStudyInstanceUid(studyInstanceUid);
|
||||||
|
|
||||||
|
const displaySetInstanceUids = study.series.map(series => {
|
||||||
|
return series.displaySetInstanceUid;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentLoadedStackIndex = displaySetInstanceUids.indexOf(displaySetInstanceUid);
|
||||||
|
|
||||||
|
const newStackIndex = indexCalculator(currentLoadedStackIndex, numberOfStacks);
|
||||||
|
if (currentLoadedStackIndex === newStackIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDisplaySetInstanceUid = displaySetInstanceUids[newStackIndex];
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
displaySetInstanceUid: newDisplaySetInstanceUid,
|
||||||
|
viewportIndex,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPreviousSeries = () => {
|
||||||
|
const indexCalculator = currentLoadedStackIndex => Math.max(currentLoadedStackIndex - 1, 0);
|
||||||
|
|
||||||
|
loadSeries(indexCalculator);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadNextSeries = () => {
|
||||||
|
const indexCalculator = (currentLoadedStackIndex, numberOfStacks) => Math.min(currentLoadedStackIndex + 1, numberOfStacks - 1);
|
||||||
|
|
||||||
|
loadSeries(indexCalculator);
|
||||||
|
};
|
||||||
|
|
||||||
|
const seriesNavigation = {
|
||||||
|
loadPreviousSeries,
|
||||||
|
loadNextSeries
|
||||||
|
};
|
||||||
|
|
||||||
|
export { seriesNavigation };
|
||||||
13
Packages/ohif-viewerbase/client/lib/sortingManager.js
Normal file
13
Packages/ohif-viewerbase/client/lib/sortingManager.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { getSortedData } from './getSortedData';
|
||||||
|
|
||||||
|
const getDisplaySets = (studyMetadata, seriesNumber, iteratorFunction) => {
|
||||||
|
const iteratorFn = typeof iteratorFunction !== 'function' ? getSortedData : iteratorFunction;
|
||||||
|
|
||||||
|
return iteratorFn(studyMetadata, seriesNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortingManager = {
|
||||||
|
getDisplaySets
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sortingManager };
|
||||||
220
Packages/ohif-viewerbase/client/lib/thumbnailDragHandlers.js
Normal file
220
Packages/ohif-viewerbase/client/lib/thumbnailDragHandlers.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
|
const cloneElement = (element, targetId) => {
|
||||||
|
// Clone the DOM element
|
||||||
|
const clone = element.cloneNode(true);
|
||||||
|
|
||||||
|
// Find any canvas children to clone
|
||||||
|
const clonedCanvases = $(clone).find('canvas');
|
||||||
|
clonedCanvases.each((canvasIndex, clonedCanvas) => {
|
||||||
|
// Draw from the original canvas to the cloned canvas
|
||||||
|
const context = clonedCanvas.getContext('2d');
|
||||||
|
const 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailDragStartHandler = (event, data) => {
|
||||||
|
// Prevent any scrolling behaviour normally caused by the original event
|
||||||
|
event.originalEvent.preventDefault();
|
||||||
|
|
||||||
|
// Identify the current study and series index from the thumbnail's DOM position
|
||||||
|
const targetThumbnail = event.currentTarget;
|
||||||
|
const $imageThumbnail = $(targetThumbnail);
|
||||||
|
|
||||||
|
// Clone the image thumbnail
|
||||||
|
const targetId = 'DragClone';
|
||||||
|
const clone = cloneElement(targetThumbnail, targetId);
|
||||||
|
const $clone = $(clone);
|
||||||
|
$clone.addClass('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 body
|
||||||
|
document.body.appendChild(clone);
|
||||||
|
|
||||||
|
// Set the cursor x and y positions from the current touch/mouse coordinates
|
||||||
|
let cursorX;
|
||||||
|
let cursorY;
|
||||||
|
// Handle touchStart cases
|
||||||
|
if (event.type === 'touchstart') {
|
||||||
|
cursorX = event.originalEvent.touches[0].pageX;
|
||||||
|
cursorY = event.originalEvent.touches[0].pageY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cursorX = event.pageX;
|
||||||
|
cursorY = event.pageY;
|
||||||
|
|
||||||
|
// Also hook up event handlers for mouse events
|
||||||
|
const handlers = {};
|
||||||
|
handlers.mousemove = event => thumbnailDragHandler(event);
|
||||||
|
handlers.mouseup = event => thumbnailDragEndHandler(event, data, handlers);
|
||||||
|
|
||||||
|
$(document).on('mousemove', handlers.mousemove);
|
||||||
|
$(document).on('mouseup', handlers.mouseup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This block gets the current offset of the touch/mouse
|
||||||
|
// relative to the window
|
||||||
|
//
|
||||||
|
// i.e. Where did the user grab it from?
|
||||||
|
const offset = $imageThumbnail.offset();
|
||||||
|
const { left, top } = offset;
|
||||||
|
|
||||||
|
// This difference is saved for later so the element movement looks normal
|
||||||
|
const diff = {
|
||||||
|
x: cursorX - left,
|
||||||
|
y: cursorY - top
|
||||||
|
};
|
||||||
|
$clone.data('diff', diff);
|
||||||
|
|
||||||
|
// This sets the default style properties of the cloned element so it is
|
||||||
|
// ready to be dragged around the page
|
||||||
|
$clone.css({
|
||||||
|
left: cursorX - diff.x,
|
||||||
|
position: 'fixed',
|
||||||
|
top: cursorY - diff.y,
|
||||||
|
visibility: 'hidden',
|
||||||
|
'z-index': 100000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailDragHandler = event => {
|
||||||
|
// Get the touch/mouse coordinates from the event
|
||||||
|
let cursorX;
|
||||||
|
let cursorY;
|
||||||
|
if (event.type === 'touchmove') {
|
||||||
|
cursorX = event.originalEvent.changedTouches[0].pageX;
|
||||||
|
cursorY = event.originalEvent.changedTouches[0].pageY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cursorX = event.pageX;
|
||||||
|
cursorY = event.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the clone element and update it's position on the page
|
||||||
|
const $clone = $('#DragClone');
|
||||||
|
const diff = $clone.data('diff');
|
||||||
|
$clone.css({
|
||||||
|
left: cursorX - diff.x,
|
||||||
|
position: 'fixed',
|
||||||
|
top: cursorY - diff.y,
|
||||||
|
visibility: 'visible',
|
||||||
|
'z-index': 100000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify the element below the current cursor position
|
||||||
|
const elemBelow = document.elementFromPoint(cursorX, cursorY);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const $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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailDragEndHandler = (event, data, handlers) => {
|
||||||
|
// Remove the mouse event listeners
|
||||||
|
if (handlers) {
|
||||||
|
$(document).off('mousemove', handlers.mousemove);
|
||||||
|
$(document).off('mouseup', handlers.mouseup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the cursor style to the default
|
||||||
|
document.body.style.cursor = 'auto';
|
||||||
|
|
||||||
|
// Get the cloned element
|
||||||
|
const $clone = $('#DragClone');
|
||||||
|
|
||||||
|
// If it doesn't exist, stop here
|
||||||
|
if (!$clone.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = $clone.offset();
|
||||||
|
const { top, left } = offset;
|
||||||
|
const diff = $clone.data('diff');
|
||||||
|
|
||||||
|
// Identify the element below the cloned element position
|
||||||
|
const elemBelow = document.elementFromPoint(left + diff.x, top + diff.y);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
let element;
|
||||||
|
const $viewportsDraggedOver = $(elemBelow).closest('.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 stored drag and drop data, stop here
|
||||||
|
if (!data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the dropped viewport index
|
||||||
|
const viewportIndex = $('.imageViewerViewport').index(element);
|
||||||
|
|
||||||
|
// Rerender the viewport using the dragged thumbnail data
|
||||||
|
OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailDragHandlers = {
|
||||||
|
thumbnailDragEndHandler,
|
||||||
|
thumbnailDragStartHandler,
|
||||||
|
thumbnailDragHandler
|
||||||
|
};
|
||||||
|
|
||||||
|
export { thumbnailDragHandlers };
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { getFrameOfReferenceUID } from './getFrameOfReferenceUID';
|
||||||
|
import { crosshairsSynchronizers } from './crosshairsSynchronizers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to maintain the updateImageSynchronizers
|
||||||
|
* that are using in the Crosshair tool. The function creates
|
||||||
|
* (and destroys any currently existing) a new synchronizer for the given
|
||||||
|
* frame of reference. It then searches for other viewports that share the same
|
||||||
|
* frame of reference, and adds those to the synchronizer. These viewports
|
||||||
|
* will now function together when the Crosshair tool is used.
|
||||||
|
*
|
||||||
|
* @param currentFrameOfReferenceUID
|
||||||
|
*/
|
||||||
|
export function updateCrosshairsSynchronizer(currentFrameOfReferenceUID) {
|
||||||
|
// Check if an old synchronizer exists, and if it does, destroy it
|
||||||
|
// If not, create a new one
|
||||||
|
let synchronizer = crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID];
|
||||||
|
if (synchronizer) {
|
||||||
|
// If it already exists, remove all source & target elements
|
||||||
|
synchronizer.destroy();
|
||||||
|
} else {
|
||||||
|
// Create a new synchronizer
|
||||||
|
crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID] = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
|
||||||
|
synchronizer = crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all elements that stem from the same frame of reference
|
||||||
|
$('.imageViewerViewport').each((index, element) => {
|
||||||
|
const frameOfReferenceUID = getFrameOfReferenceUID(element);
|
||||||
|
if (currentFrameOfReferenceUID !== frameOfReferenceUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronizer.add(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the orientation labels on a Cornerstone-enabled Viewport element
|
||||||
|
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
|
||||||
|
*
|
||||||
|
* @param element The DOM element of the Cornerstone viewport
|
||||||
|
* optional
|
||||||
|
* @param viewport The current viewport
|
||||||
|
*/
|
||||||
|
export function updateOrientationMarkers(element, viewport) {
|
||||||
|
// Get the current viewport settings
|
||||||
|
if(!viewport) {
|
||||||
|
viewport = cornerstone.getViewport(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the orientation labels on the viewport
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
const imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||||
|
|
||||||
|
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
||||||
|
const columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
||||||
|
const oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
||||||
|
const oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
||||||
|
|
||||||
|
const 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
|
||||||
|
const viewportOrientationMarkers = $(element).siblings('.viewportOrientationMarkers');
|
||||||
|
const topMarker = viewportOrientationMarkers.find('.topMid');
|
||||||
|
const 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Packages/ohif-viewerbase/client/lib/viewportOverlayUtils.js
Normal file
100
Packages/ohif-viewerbase/client/lib/viewportOverlayUtils.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { getElementIfNotEmpty } from './getElementIfNotEmpty';
|
||||||
|
|
||||||
|
const getPatient = function(property) {
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||||
|
if (!patient) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return patient[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStudy = function(property) {
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||||
|
if (!study) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return study[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSeries = function(property) {
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||||
|
if (!series) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return series[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstance = function(property) {
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||||
|
if (!instance) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTagDisplay = function(property) {
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = cornerstoneTools.metaData.get('tagDisplay', this.imageId);
|
||||||
|
if (!instance) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImage = function(viewportIndex) {
|
||||||
|
const element = getElementIfNotEmpty(viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabledElement;
|
||||||
|
try {
|
||||||
|
enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
} catch(error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabledElement || !enabledElement.image) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledElement.image;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (date, time) => `${date} ${time}`;
|
||||||
|
|
||||||
|
const viewportOverlayUtils = {
|
||||||
|
getPatient,
|
||||||
|
getStudy,
|
||||||
|
getSeries,
|
||||||
|
getInstance,
|
||||||
|
getTagDisplay,
|
||||||
|
getImage,
|
||||||
|
formatDateTime
|
||||||
|
};
|
||||||
|
|
||||||
|
export { viewportOverlayUtils };
|
||||||
268
Packages/ohif-viewerbase/client/lib/viewportUtils.js
Normal file
268
Packages/ohif-viewerbase/client/lib/viewportUtils.js
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { Session } from 'meteor/session';
|
||||||
|
import { Random } from 'meteor/random';
|
||||||
|
import { $ } from 'meteor/jquery';
|
||||||
|
import { _ } from 'meteor/underscore';
|
||||||
|
// Local Modules
|
||||||
|
import { updateOrientationMarkers } from './updateOrientationMarkers';
|
||||||
|
import { getInstanceClassDefaultViewport } from './instanceClassSpecificViewport';
|
||||||
|
|
||||||
|
function getActiveViewportElement() {
|
||||||
|
const viewportIndex = Session.get('activeViewport') || 0;
|
||||||
|
return $('.imageViewerViewport').get(viewportIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
const scaleIncrement = 0.15;
|
||||||
|
const maximumScale = 10;
|
||||||
|
viewport.scale = Math.min(viewport.scale + scaleIncrement, maximumScale);
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
const scaleIncrement = 0.15;
|
||||||
|
const minimumScale = 0.05;
|
||||||
|
viewport.scale = Math.max(viewport.scale - scaleIncrement, minimumScale);
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomToFit() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.fitToWindow(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateL() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.rotation -= 90;
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
updateOrientationMarkers(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateR() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.rotation += 90;
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
updateOrientationMarkers(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invert() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.invert = (viewport.invert === false);
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flipV() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.vflip = (viewport.vflip === false);
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
updateOrientationMarkers(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flipH() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
let viewport = cornerstone.getViewport(element);
|
||||||
|
viewport.hflip = (viewport.hflip === false);
|
||||||
|
cornerstone.setViewport(element, viewport);
|
||||||
|
updateOrientationMarkers(element, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetViewport() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
if (enabledElement.fitToWindow === false) {
|
||||||
|
const imageId = enabledElement.image.imageId;
|
||||||
|
const instance = cornerstoneTools.metaData.get('instance', imageId);
|
||||||
|
|
||||||
|
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, enabledElement.image);
|
||||||
|
|
||||||
|
const instanceClassDefaultViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
|
||||||
|
cornerstone.setViewport(element, instanceClassDefaultViewport);
|
||||||
|
} else {
|
||||||
|
cornerstone.reset(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTools() {
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
toolStateManager.clear(element);
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function was originally defined alone inside client/lib/toggleDialog.js
|
||||||
|
// and has been moved here to avoid circular dependency issues.
|
||||||
|
function toggleDialog(element) {
|
||||||
|
const $element = $(element);
|
||||||
|
if($element.is('dialog')) {
|
||||||
|
if (element.hasAttribute('open')) {
|
||||||
|
stopAllClips();
|
||||||
|
element.close();
|
||||||
|
} else {
|
||||||
|
element.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const isClosed = $element.hasClass('dialog-open');
|
||||||
|
$element.toggleClass('dialog-closed', isClosed);
|
||||||
|
$element.toggleClass('dialog-open', !isClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.set('UpdateCINE', Random.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the play/stop state for the cornerstone clip tool
|
||||||
|
function toggleCinePlay() {
|
||||||
|
// Get the active viewport element
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
|
||||||
|
// Check if it's playing the clip to toggle it
|
||||||
|
if (isPlaying()) {
|
||||||
|
cornerstoneTools.stopClip(element);
|
||||||
|
} else {
|
||||||
|
cornerstoneTools.playClip(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the UpdateCINE session property
|
||||||
|
Session.set('UpdateCINE', Random.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide the CINE dialog
|
||||||
|
function toggleCineDialog() {
|
||||||
|
var dialog = document.getElementById('cineDialog');
|
||||||
|
toggleDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the clip is playing on the active viewport
|
||||||
|
function isPlaying() {
|
||||||
|
// Create a dependency on LayoutManagerUpdated and UpdateCINE session
|
||||||
|
Session.get('UpdateCINE');
|
||||||
|
Session.get('LayoutManagerUpdated');
|
||||||
|
|
||||||
|
// Get the viewport element and its current playClip tool state
|
||||||
|
const element = getActiveViewportElement();
|
||||||
|
// Empty Elements throws cornerstore exception
|
||||||
|
if (!element || !$(element).find('canvas').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'playClip');
|
||||||
|
|
||||||
|
// Stop here if the tool state is not defined yet
|
||||||
|
if (!toolState) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the clip state
|
||||||
|
const clipState = toolState.data[0];
|
||||||
|
|
||||||
|
if(clipState) {
|
||||||
|
// Return true if the clip is playing
|
||||||
|
return !_.isUndefined(clipState.intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a study has multiple frames
|
||||||
|
function hasMultipleFrames() {
|
||||||
|
// Its called everytime active viewport and/or layout change
|
||||||
|
Session.get('activeViewport');
|
||||||
|
Session.get('LayoutManagerUpdated');
|
||||||
|
|
||||||
|
const activeViewport = getActiveViewportElement();
|
||||||
|
|
||||||
|
// No active viewport yet: disable button
|
||||||
|
if(!activeViewport || !$(activeViewport).find('canvas').length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get images in the stack
|
||||||
|
const stackToolData = cornerstoneTools.getToolState(activeViewport, 'stack');
|
||||||
|
|
||||||
|
// No images in the stack, so disable button
|
||||||
|
if (!stackToolData || !stackToolData.data || !stackToolData.data.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get number of images in the stack
|
||||||
|
const stackData = stackToolData.data[0];
|
||||||
|
const nImages = stackData.imageIds && stackData.imageIds.length ? stackData.imageIds.length : 1;
|
||||||
|
|
||||||
|
// Stack has just one image, so disable button
|
||||||
|
if(nImages === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop clips on all non-empty elements
|
||||||
|
function stopAllClips() {
|
||||||
|
const elements = $('.imageViewerViewport').not('.empty');
|
||||||
|
elements.each( (index, element) => {
|
||||||
|
if ($(element).find('canvas').length) {
|
||||||
|
cornerstoneTools.stopClip(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Create an event listener to update playing state when a clip stops playing
|
||||||
|
$(window).on('CornerstoneToolsClipStopped', () => Session.set('UpdateCINE', Random.id()));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export functions inside viewportUtils namespace.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const viewportUtils = {
|
||||||
|
getActiveViewportElement,
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
zoomToFit,
|
||||||
|
rotateL,
|
||||||
|
rotateR,
|
||||||
|
invert,
|
||||||
|
flipV,
|
||||||
|
flipH,
|
||||||
|
resetViewport,
|
||||||
|
clearTools,
|
||||||
|
toggleDialog,
|
||||||
|
toggleCinePlay,
|
||||||
|
toggleCineDialog,
|
||||||
|
isPlaying,
|
||||||
|
hasMultipleFrames,
|
||||||
|
stopAllClips
|
||||||
|
};
|
||||||
|
|
||||||
|
export { viewportUtils };
|
||||||
39
Packages/ohif-viewerbase/main.js
Normal file
39
Packages/ohif-viewerbase/main.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Import namespace...
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OHIF, Viewerbase } from './namespace.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import scripts that will populate the Viewerbase namespace as a side effect only import. This is effectively the public API...
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './client/'; // which is actually: import './client/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export relevant objects...
|
||||||
|
*
|
||||||
|
* With the following export it becomes possible to import "OHIF" from "ohif:core" and "Viewerbase"
|
||||||
|
* from "ohif:viewerbase" using a single import (a shorthand), like this:
|
||||||
|
*
|
||||||
|
* import { OHIF } from 'meteor/ohif:viewerbase';
|
||||||
|
*
|
||||||
|
* Which is equivalent to:
|
||||||
|
*
|
||||||
|
* import { OHIF } from 'meteor/ohif:core';
|
||||||
|
* import 'meteor/ohif:viewerbase';
|
||||||
|
*
|
||||||
|
* The second (extended) format should be used when other OHIF packages are also to be used within
|
||||||
|
* the current module. This makes it explicit that the following imports will populate their
|
||||||
|
* respective namespaces within the to "OHIF" namespace. Example:
|
||||||
|
*
|
||||||
|
* import { OHIF } from 'meteor/ohif:core';
|
||||||
|
* import 'meteor/ohif:viewerbase';
|
||||||
|
* import 'meteor/ohif:hanging-protocols';
|
||||||
|
* [ ... ]
|
||||||
|
* OHIF.viewerbase.setActiveViewport(...);
|
||||||
|
* OHIF.hangingprotocols.doSomething(...);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { OHIF, Viewerbase };
|
||||||
23
Packages/ohif-viewerbase/namespace.js
Normal file
23
Packages/ohif-viewerbase/namespace.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Import main dependency...
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Viewerbase namespace...
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Viewerbase = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append Viewerbase namespace to OHIF namespace...
|
||||||
|
*/
|
||||||
|
|
||||||
|
OHIF.viewerbase = Viewerbase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export relevant objects...
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { OHIF, Viewerbase };
|
||||||
Loading…
Reference in New Issue
Block a user