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
|
||||
if (Meteor.isDevelopment && Meteor.isClient) {
|
||||
// @TODO: remove this after applying namespace to this package
|
||||
if (Meteor.isClient) {
|
||||
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