continue consolidating packages, converting various pieces to react

This commit is contained in:
Erik Ziegler committed 2018-12-13 23:24:39 +01:00
1 parent 5f30f7bd35
commit 7b66dec163
90 files changed
+350 -485

No files matched your search

@@ -1,69 +0,0 @@
import { OHIF } from 'meteor/ohif:core';
import { OHIFError } from './OHIFError';
const OBJECT = 'object';
/**
* 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 OHIFError('ImageSet expects an array of images');
}
// @property "images"
Object.defineProperty(this, 'images', {
enumerable: false,
configurable: false,
writable: false,
value: images
});
// @property "uid"
Object.defineProperty(this, 'uid', {
enumerable: false,
configurable: false,
writable: false,
value: OHIF.utils.guid() // Unique ID of the instance
});
}
getUID() {
return this.uid;
}
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);
}
}
@@ -674,3 +674,4 @@ export class LayoutManager {
return this.layoutProps.row !== 1 && this.layoutProps.columns !== 1;
}
}
@@ -1,14 +0,0 @@
// @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;
}
}
@@ -1,167 +0,0 @@
# Table of contents
In this document, some important objects are described. In the files there are comments that can help better undestand their methods and properties.
- [ResizeViewportManager object](#the-resize-viewport-manager-object)
- [ImageSet object](#the-image-set-object)
- [Layout Manager](#the-layout-manager-object)
- [Type Safe Collections](#the-type-safe-collections)
# The Resize Viewport Manager object
This object has multiple functions to manage window resize event. It relocates Dialogs, resizes viewport elements and scrollbars and some other UI components such as Study and Series Quick Switch, when available.
## Usage
It's only necessary to bind **handleResize** function to the window resize event as follows. The **ohif:viewerbase** package needs to be imported by the referring code as well.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const ResizeViewportManager = new Viewerbase.ResizeViewportManager();
window.addEventListener('resize', ResizeViewportManager.getResizeHandler());
```
An example os its usage can be found in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
# The Image Set object
An object that 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).
## Usage
ImageSet constructor requires an array of SOP instances like in the example below. It's necessary to import **ohif:viewerbase**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const imageSet = new Viewerbase.ImageSet(sopInstances);
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid,
seriesInstanceUid: seriesData.seriesInstanceUid,
seriesNumber: seriesData.seriesNumber,
seriesDescription: seriesData.seriesDescription,
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: seriesData.modality,
isMultiFrame: isMultiFrame(instance)
});
// Sort instances by InstanceNumber (0020,0013)
imageSet.sortBy((a, b) => {
return (parseInt(a.getRawValue('x00200013', 0)) || 0) - (parseInt(b.getRawValue('x00200013', 0)) || 0);
});
```
Each SOP instance in this example is an instance of **OHIFInstanceMetadata** object, which is a specialization of **InstanceMetadata**. To read more about the **Metadata API** click [here](metadata/).
# The Layout Manager object
Objects of this class are responsible for creating, organizing and maintaining (manage) viewport rendering. It creates a grid, positioning viewports accordingly to it's configuration keeping all viewports data (in **viewportData** property) for easy access from other components. It support many layout configurations and some of them were fully tested: 1x1, 1x2, 1x3, 2x1, 2x2, 2x3, 3x1, 3x2, 3x3. Other configurations may work as well.
Finally it provides some useful functions to move through viewports and zoom it.
## Usage
In order to use _LayoutManager_ the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows. An example os its usage is in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new Viewerbase.LayoutManager(parentElement, studies);
```
The default configuration is 1x1, and to change it just set **layoutProps** and call **updateViewports** to update the layout as follows.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new LayoutManager(parentElement, studies);
// Set the layout proprerties to 2x2 layout
LayoutManager.layoutProps = {
rows: 2,
columns: 2
};
// It will render four viewports: two in each row.
LayoutManager.updateViewports();
```
The layoutManagerTarget element will have a new class **layout-2-2** (to allow further styling) and it's inner content will a new div#imageViewerViewports that has four inner elements like the following (some elements and attributes were removed for example purpose):
```html
<div class="viewportContainer active" style="height:50%; width:50%;">
<div class="removable">
<div class="imageViewerViewport">
<canvas></canvas>
</div>
<div class="imageViewerViewportOverlay"></div>
<div class="imageViewerLoadingIndicator"></div>
<div class="imageViewerErrorLoadingIndicator"></div>
<div class="viewportOrientationMarkers"></div>
</div>
</div>
```
Each of this _div.viewportContainer_ will have some classes to help CSS specific styling accordingly to the element's position in the grid: **top**, **middle** and **bottom**. This classes are added by **viewer/components/gridLayout/** component in ohif-viewerbase package.
# The Type Safe Collections
With the introduction of the new _Study Metadata API_ in which study metadata is represented by class hierarchies (using prototype-based inheritance), the usage of standard _Minimongo_ collections as a central client-side storage for this data became no longer an option. Standard _Mongo_ and _Minimongo_ collections internally _flatten_ data (in other words, data gets serialized) before storage hence no functions or prototype chains are preserved. In that scenario, when an object is restored (fetched), what is returned is actually a flattened copy of the original object with no functions or prototype (it's no longer an instance of it's original class). As an attempt to overcome this limitation a new type of collection was intruduced: the *TypeSafeCollection*.
The `TypeSafeCollection` is a simple list-like collection which tries to implement an API _similar_ but not compatible with _Mongo_'s API. It supports basic features like search by attribute map and ID, retrieval by index, sorting of result sets, insertion, removal and reactive operations but, unlike _Mongo_'s API, it (still) lacks support to advanced functionality like complex search criterea or flexible sorting options.
## Implementation
The `TypeSafeCollection` is implemented on top of the _JavaScript_ `Array` object. Each element inserted in the collection is appended to the end of its internal array as a _key-value pair (KVP)_ object where the _key_ is a unique randomly generated ID string and the _value_ is the element itself. Once the object has been successfully stored, the generated ID (its ID) is returned to the client code and can later be used to access that specific element. At this point, an important difference to the _Minimongo_ API can be highlighted: a _TypeSafeCollection_ instance will never make any changes to the stored element (e.g., no "\_id" property will ever be assigned to the original object). Another relevant feature that is supported by this design decision is that _not only objects_ can be stored in this collections, but literally _anything_.
Inside the codebase, the _value_ attribute of each _KVP_ entry in the collection is refered to as _the **payload** of the entry_ since it's what really matters to the user. Hence, this term will also be used here to refer to the _value that has been stored in the collection_. That being said, we can approach another important feature of these collections: A single _payload_ cannot be stored more than once in a given collection. When an attempt of inserting a _payload_ which is already present in the collection is detected, the insert operation will fail and `null` will be returned. In that regard, the collection behaves like `Set` object not permitting a payload to be stored more than once. Strict equality is used when comparing payloads, thus cloned objects are not considered the same. This feature adds an additional garantee that a given study/series/instance will not be listed more than once (it was designed as a replacement for central study collections which were always checked for duplicates).
Please refer to the codebase for the full `TypeSafeCollection` API.
## Usage
In order to use the `TypeSafeCollection` class, the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows:
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase'; // i.e., Viewerbase.TypeSafeCollection
OR
import { OHIF } from 'meteor/ohif:core';
import 'meteor/ohif:viewerbase'; // i.e., OHIF.viewerbase.TypeSafeCollection
// The later is preferred when the client code already makes use of the "OHIF" namespace making the second
// "import" a garantee that the ".viewerbase" namespace has been properly loaded.
```
A few usage examples:
```javascript
const Users = new OHIF.viewerbase.TypeSafeCollection();
[[ ... ]]
// Insert a User object...
let userId = Users.insert({
data: {
firstName: 'John',
lastName: 'Doe',
age: 45
},
getFullName() {
return `${this.data.firstName} ${this.data.lastName}`;
},
getAge() {
return this.data.age;
}
});
[[ ... ]]
let theUserWeJustStored = Users.findById(userId); // ;-)
[[ ... ]]
// Retrieve a single user with "Doe" as `lastName`...
let myUser = Users.findBy({ 'data.lastName': 'Doe' });
// Or all users with "Doe" as `lastName`, sorted by `firstName` in ascending
// order and using the `age` attribute to break ties in descending order...
let myUsers = Users.findAllBy({ 'data.lastName': 'Doe' }, {
sort: [ [ 'data.firstName', 'asc' ], [ 'data.age', 'desc' ] ]
});
```
@@ -1,151 +0,0 @@
import { Session } from 'meteor/session';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { getInstanceClassDefaultViewport } from '../instanceClassSpecificViewport';
// Manage resizing viewports triggered by window resize
export class ResizeViewportManager {
constructor() {
this._resizeHandler = null;
}
// Reposition Study Series Quick Switch based whether side bars are opened or not
repositionStudySeriesQuickSwitch() {
OHIF.log.info('ResizeViewportManager repositionStudySeriesQuickSwitch');
// Stop here if viewer is not displayed
const isViewer = Session.get('ViewerOpened');
if (!isViewer) return;
// Stop here if there is no one or only one viewport
const nViewports = OHIF.viewerbase.layoutManager.viewportData.length;
if (!nViewports || nViewports <= 1) return;
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 = $('#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');
$currentOverlay.find('.scrollbar').trigger('rescale');
}
// Resize a single viewport element
resizeViewportElement(element, fitToWindow = true) {
let enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
cornerstone.resize(element, fitToWindow);
if (enabledElement.fitToWindow === false) {
const imageId = enabledElement.image.imageId;
const instance = cornerstone.metaData.get('instance', imageId);
const instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
cornerstone.setViewport(element, instanceClassViewport);
}
}
// Resize each viewport element
resizeViewportElements() {
this.relocateDialogs();
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);
}
/**
* Returns a unique event handler function associated with a given instance using lazy assignment.
* @return {function} Returns a unique copy of the event handler of this class.
*/
getResizeHandler() {
let resizeHandler = this._resizeHandler;
if (resizeHandler === null) {
resizeHandler = this.handleResize.bind(this);
this._resizeHandler = resizeHandler;
}
return resizeHandler;
}
}
@@ -1,218 +0,0 @@
import { Session } from 'meteor/session';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolManager } from '../toolManager';
export class StackImagePositionOffsetSynchronizer {
constructor() {
this.active = false;
this.syncedViewports = [];
this.synchronizer = new cornerstoneTools.Synchronizer('cornerstonenewimage', cornerstoneTools.stackImagePositionOffsetSynchronizer);
}
static get ELEMENT_DISABLED_EVENT() {
return 'cornerstoneelementdisabled.StackImagePositionOffsetSynchronizer';
}
isActive() {
return this.active;
}
activate() {
const viewports = this.getLinkableViewports();
this.syncViewports(viewports);
}
activateByViewportIndexes(viewportIndexes) {
const viewports = this.getViewportByIndexes(viewportIndexes);
this.syncViewports(viewports);
}
deactivate() {
if (!this.isActive()) {
return;
}
while (this.syncedViewports.length) {
const viewport = this.syncedViewports[0];
this.removeViewport(viewport);
}
this.active = false;
toolManager.deactivateCommandButton('linkStackScroll');
}
update() {
if (!this.isActive()) {
return;
}
const activeViewportElement = this.getActiveViewportElement();
if (this.isViewportSynced(activeViewportElement)) {
return;
}
this.deactivate();
this.activate();
}
syncViewports(viewports) {
const viewportIndexes = [];
if (this.isActive() || (viewports.length <= 1)) {
return;
}
viewports.forEach((viewport, index) => {
this.synchronizer.add(viewport.element);
this.syncedViewports.push(viewport);
viewportIndexes.push(viewport.index);
if (!this.disabledListener) {
this.disabledListener = this.elementDisabledHandler(this);
}
viewport.element.addEventListener(StackImagePositionOffsetSynchronizer.ELEMENT_DISABLED_EVENT, this.disabledListener);
});
this.active = true;
toolManager.activateCommandButton('linkStackScroll');
Session.set('StackImagePositionOffsetSynchronizerLinkedViewports', viewportIndexes);
}
isViewportSynced(viewportElement) {
return !!this.getViewportByElement(viewportElement);
}
getActiveViewportElement() {
const viewportIndex = window.store.getState().viewports.activeViewport || 0;
return $('.imageViewerViewport').get(viewportIndex);
}
removeViewport(viewport) {
const index = this.syncedViewports.indexOf(viewport);
if (index === -1) {
return;
}
this.syncedViewports.splice(index, 1);
this.synchronizer.remove(viewport.element);
this.removeLinkedViewportFromSession(viewport);
viewport.element.removeEventListener(StackImagePositionOffsetSynchronizer.ELEMENT_DISABLED_EVENT, this.disabledListener);
}
getViewportByElement(viewportElement) {
const length = this.syncedViewports.length;
for (let i = 0; i < length; i++) {
const viewport = this.syncedViewports[i];
if (viewport.element === viewportElement) {
return viewport;
}
}
}
removeViewportByElement(viewportElement) {
let viewport = this.getViewportByElement(viewportElement);
if (viewport) {
this.removeViewport(viewport);
}
}
removeLinkedViewportFromSession(viewport) {
const linkedViewports = Session.get('StackImagePositionOffsetSynchronizerLinkedViewports');
const index = linkedViewports.indexOf(viewport.index);
if (index !== -1) {
linkedViewports.splice(index, 1);
Session.set('StackImagePositionOffsetSynchronizerLinkedViewports', linkedViewports);
}
}
elementDisabledHandler(context) {
return e => context.removeViewportByElement(e.detail.element);
}
getViewportByIndexes(viewportIndexes) {
const viewports = [];
const $viewportElements = $('.imageViewerViewport');
viewportIndexes.forEach(index => {
const element = $viewportElements.get(index);
if (!element) {
return;
}
viewports.push({
index,
element
});
});
return viewports;
}
isViewportsLinkable(viewportElementA, viewportElementB) {
const viewportAImageNormal = this.getViewportImageNormal(viewportElementA);
const viewportBImageNormal = this.getViewportImageNormal(viewportElementB);
if (viewportAImageNormal && viewportBImageNormal) {
const angleInRadians = viewportBImageNormal.angleTo(viewportAImageNormal);
// Pi / 12 radians = 15 degrees
// If the angle between two vectors is Pi, it means they are just inverted
return angleInRadians < Math.PI / 12 || angleInRadians === Math.PI;
}
return false;
}
getLinkableViewports() {
const activeViewportElement = this.getActiveViewportElement();
const viewports = [];
$('.imageViewerViewport').each((index, viewportElement) => {
if (this.isViewportsLinkable(activeViewportElement, viewportElement)) {
viewports.push({
index: index,
element: viewportElement
});
}
});
return viewports;
}
getViewportImageNormal(element) {
if (!element) {
return;
}
element = $(element).get(0);
try {
const enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement.image) {
return;
}
const imageId = enabledElement.image.imageId;
const imagePlane = cornerstone.metaData.get('imagePlane', imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
return imagePlane.rowCosines.clone().cross(imagePlane.columnCosines);
} catch(error) {
const errorMessage = error.message || error;
OHIF.log.info(`StackImagePositionOffsetSynchronizer getViewportImageNormal: ${errorMessage}`);
}
}
}
@@ -1,398 +0,0 @@
import $ from 'jquery';
import { Session } from 'meteor/session';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone, cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone';
class BaseLoadingListener {
constructor(stack, options) {
options = options || {};
this.id = BaseLoadingListener.getNewId();
this.stack = stack;
this.startListening();
this.statsItemsLimit = options.statsItemsLimit || 2;
this.stats = {
items: [],
total: 0,
elapsedTime: 0,
speed: 0
};
// Register the start point to make it possible to calculate
// bytes/s or frames/s when the first byte or frame is received
this._addStatsData(0);
// Update the progress before starting the download
// to make it possible to update the UI
this._updateProgress();
}
_addStatsData(value) {
const date = new Date();
const stats = this.stats;
const items = stats.items;
const newItem = {
value,
date
};
items.push(newItem);
stats.total += newItem.value;
// Remove items until it gets below the limit
while (items.length > this.statsItemsLimit) {
const item = items.shift();
stats.total -= item.value;
}
// Update the elapsedTime (seconds) based on first and last
// elements and recalculate the speed (bytes/s or frames/s)
if (items.length > 1) {
const oldestItem = items[0];
stats.elapsedTime = (newItem.date.getTime() - oldestItem.date.getTime()) / 1000;
stats.speed = (stats.total - oldestItem.value) / stats.elapsedTime;
}
}
_getProgressSessionId() {
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
return 'StackProgress:' + displaySetInstanceUid;
}
_clearSession() {
const progressSessionId = this._getProgressSessionId();
Session.set(progressSessionId, undefined);
delete Session.keys.progressSessionId;
}
startListening() {
throw new Error('`startListening` must be implemented by child clases');
}
stopListening() {
throw new Error('`stopListening` must be implemented by child clases');
}
destroy() {
this.stopListening();
this._clearSession();
}
static getNewId() {
const timeSlice = (new Date()).getTime().toString().slice(-8);
const randomNumber = parseInt(Math.random() * 1000000000);
return timeSlice.toString() + randomNumber.toString();
}
}
class DICOMFileLoadingListener extends BaseLoadingListener {
constructor(stack) {
super(stack);
this._dataSetUrl = this._getDataSetUrl(stack);
this._lastLoaded = 0;
// Check how many instances has already been download (cached)
this._checkCachedData();
}
_checkCachedData() {
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(this._dataSetUrl);
if (dataSet) {
const dataSetLength = dataSet.byteArray.length;
this._updateProgress({
percentComplete: 100,
loaded: dataSetLength,
total: dataSetLength
});
}
}
_getImageLoadProgressEventName() {
return 'cornerstoneimageloadprogress.' + this.id;
}
startListening() {
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
const imageLoadProgressEventHandle = this._imageLoadProgressEventHandle.bind(this);
this.stopListening();
cornerstone.events.addEventListener(imageLoadProgressEventName, imageLoadProgressEventHandle);
}
stopListening() {
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
cornerstone.events.removeEventListener(imageLoadProgressEventName);
}
_imageLoadProgressEventHandle(e) {
const eventData = e.detail;
const dataSetUrl = this._convertImageIdToDataSetUrl(eventData.imageId);
const bytesDiff = eventData.loaded - this._lastLoaded;
if (!this._dataSetUrl === dataSetUrl) {
return;
}
// Add the bytes downloaded to the stats
this._addStatsData(bytesDiff);
// Update the download progress
this._updateProgress(eventData);
// Cache the last eventData.loaded value
this._lastLoaded = eventData.loaded;
}
_updateProgress(eventData) {
const progressSessionId = this._getProgressSessionId();
eventData = eventData || {};
Session.set(progressSessionId, {
multiFrame: false,
percentComplete: eventData.percentComplete,
bytesLoaded: eventData.loaded,
bytesTotal: eventData.total,
bytesPerSecond: this.stats.speed
});
}
_convertImageIdToDataSetUrl(imageId) {
// Remove the prefix ("dicomweb:" or "wadouri:"")
imageId = imageId.replace(/^(dicomweb:|wadouri:)/i, '');
// Remove "frame=999&" from the imageId
imageId = imageId.replace(/frame=\d+&?/i, '');
// Remove the last "&" like in "http://...?foo=1&bar=2&"
imageId = imageId.replace(/&$/, '');
return imageId;
}
_getDataSetUrl(stack) {
const imageId = stack.imageIds[0];
return this._convertImageIdToDataSetUrl(imageId);
}
}
class StackLoadingListener extends BaseLoadingListener {
constructor(stack) {
super(stack, { statsItemsLimit: 20 });
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
this.framesStatus = this._createArray(stack.imageIds.length, false);
this.loadedCount = 0;
// Check how many instances has already been download (cached)
this._checkCachedData();
}
_convertImageIdsArrayToMap(imageIds) {
const imageIdsMap = new Map();
for (let i = 0; i < imageIds.length; i++) {
imageIdsMap.set(imageIds[i], {
index: i,
loaded: false
});
}
return imageIdsMap;
}
_createArray(length, defaultValue) {
// `new Array(length)` is an anti-pattern in javascript because its
// funny API. Otherwise I would go for `new Array(length).fill(false)`
const array = [];
for (let i = 0; i < length; i++) {
array[i] = defaultValue;
}
return array;
}
_checkCachedData() {
// const imageIds = this.stack.imageIds;
// TODO: No way to check status of Promise.
/*for(let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imagePromise = cornerstone.imageCache.getImageLoadObject(imageId).promise;
if (imagePromise && (imagePromise.state() === 'resolved')) {
this._updateFrameStatus(imageId, true);
}
}*/
}
_getImageLoadedEventName() {
return 'cornerstoneimageloaded.' + this.id;
}
_getImageCachePromiseRemoveEventName() {
return 'cornerstoneimagecachepromiseremoved.' + this.id;
}
startListening() {
const imageLoadedEventName = this._getImageLoadedEventName();
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
const imageLoadedEventHandle = this._imageLoadedEventHandle.bind(this);
const imageCachePromiseRemovedEventHandle = this._imageCachePromiseRemovedEventHandle.bind(this);
this.stopListening();
cornerstone.events.addEventListener(imageLoadedEventName, imageLoadedEventHandle);
cornerstone.events.addEventListener(imageCachePromiseRemovedEventName, imageCachePromiseRemovedEventHandle);
}
stopListening() {
const imageLoadedEventName = this._getImageLoadedEventName();
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
cornerstone.events.removeEventListener(imageLoadedEventName);
cornerstone.events.removeEventListener(imageCachePromiseRemovedEventName);
}
_updateFrameStatus(imageId, loaded) {
const imageData = this.imageDataMap.get(imageId);
if (!imageData || (imageData.loaded === loaded)) {
return;
}
// Add one more frame to the stats
if (loaded) {
this._addStatsData(1);
}
imageData.loaded = loaded;
this.framesStatus[imageData.index] = loaded;
this.loadedCount += loaded ? 1 : -1;
this._updateProgress();
}
_imageLoadedEventHandle(e) {
this._updateFrameStatus(e.detail.image.imageId, true);
}
_imageCachePromiseRemovedEventHandle(e) {
this._updateFrameStatus(e.detail.imageId, false);
}
_updateProgress() {
const totalFramesCount = this.stack.imageIds.length;
const loadedFramesCount = this.loadedCount;
const loadingFramesCount = totalFramesCount - loadedFramesCount;
const percentComplete = Math.round(loadedFramesCount / totalFramesCount * 100);
const progressSessionId = this._getProgressSessionId();
Session.set(progressSessionId, {
multiFrame: true,
totalFramesCount,
loadedFramesCount,
loadingFramesCount,
percentComplete,
framesPerSecond: this.stats.speed,
framesStatus: this.framesStatus
});
}
_logProgress() {
const totalFramesCount = this.stack.imageIds.length;
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
let progressBar = '[';
for (let i = 0; i < totalFramesCount; i++) {
const ch = this.framesStatus[i] ? '|' : '.';
progressBar += `${ch}`;
}
progressBar += ']';
OHIF.log.info(`${displaySetInstanceUid}: ${progressBar}`);
}
}
class StudyLoadingListener {
constructor() {
this.listeners = {};
}
addStack(stack, stackMetaData) {
const displaySetInstanceUid = stack.displaySetInstanceUid;
if (!this.listeners[displaySetInstanceUid]) {
const listener = this._createListener(stack, stackMetaData);
if (listener) {
this.listeners[displaySetInstanceUid] = listener;
}
}
}
addStudy(study) {
study.displaySets.forEach(displaySet => {
const stack = OHIF.viewerbase.stackManager.findOrCreateStack(study, displaySet);
this.addStack(stack, {
isMultiFrame: displaySet.isMultiFrame
});
});
}
addStudies(studies) {
if (!studies || !studies.length) {
return;
}
studies.forEach(study => this.addStudy(study));
}
clear() {
const displaySetInstanceUids = Object.keys(this.listeners);
const length = displaySetInstanceUids.length;
for (let i = 0; i < length; i++) {
const displaySetInstanceUid = displaySetInstanceUids[i];
const displaySet = this.listeners[displaySetInstanceUid];
displaySet.destroy();
}
this.listeners = {};
}
_createListener(stack, stackMetaData) {
const schema = this._getSchema(stack);
// A StackLoadingListener can be created if it's wadors or not a multiframe
// wadouri instance (single file) that means "N" files will have to be
// downloaded where "N" is the number of frames. DICOMFileLoadingListener
// is created only if it's a single DICOM file and there's no way to know
// how many frames has already been loaded (bytes/s instead of frames/s).
if ((schema === 'wadors') || !stackMetaData.isMultiFrame) {
return new StackLoadingListener(stack);
} else {
return new DICOMFileLoadingListener(stack);
}
}
_getSchema(stack) {
const imageId = stack.imageIds[0];
const colonIndex = imageId.indexOf(':');
return imageId.substring(0, colonIndex);
}
// Singleton
static getInstance() {
if (!StudyLoadingListener._instance) {
StudyLoadingListener._instance = new StudyLoadingListener();
}
return StudyLoadingListener._instance;
}
}
export { StudyLoadingListener, StackLoadingListener, DICOMFileLoadingListener };
@@ -1,30 +0,0 @@
import { OHIFError } from './OHIFError';
/**
* Abstract class to fetch study metadata.
*/
export class StudyMetadataSource {
/**
* Get study metadata for a study with given study InstanceUID.
* @param {String} studyInstanceUID Study InstanceUID.
*/
getByInstanceUID(studyInstanceUID) {
/**
* Please override this method on a specialized class.
*/
throw new OHIFError('StudyMetadataSource::getByInstanceUID is not overriden. Please, override it in a specialized class. See OHIFStudyMetadataSource for example');
}
/**
* Load study info and study metadata for a given study into the viewer.
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
*/
loadStudy(study) {
/**
* Please override this method on a specialized class.
*/
throw new OHIFError('StudyMetadataSource::loadStudy is not overriden. Please, override it in a specialized class. See OHIFStudyMetadataSource for example');
}
}
@@ -1,330 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import $ from 'jquery';
import _ from 'underscore';
import { OHIF } from 'meteor/ohif:core';
import { OHIFError } from './OHIFError';
import { StackManager } from '../StackManager.js';
import { getImageId } from '../getImageId.js';
export class StudyPrefetcher {
constructor(studies) {
this.studies = studies || [];
this.prefetchDisplaySetsTimeout = 300;
this.lastActiveViewportElement = null;
this.cacheFullHandlerBound = _.bind(this.cacheFullHandler, this);
cornerstone.events.addEventListener('cornerstoneimagecachefull.StudyPrefetcher', this.cacheFullHandlerBound);
}
destroy() {
this.stopPrefetching();
cornerstone.events.removeEventListener('cornerstoneimagecachefull.StudyPrefetcher', this.cacheFullHandlerBound);
}
static getInstance() {
if (!StudyPrefetcher.instance) {
StudyPrefetcher.instance = new StudyPrefetcher();
}
return StudyPrefetcher.instance;
}
setStudies(studies) {
this.stopPrefetching();
this.studies = studies;
}
prefetch() {
if (!this.studies || !this.studies.length) {
return;
}
this.stopPrefetching();
this.prefetchActiveViewport();
this.prefetchDisplaySets();
}
stopPrefetching() {
this.disableViewportPrefetch();
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
}
prefetchActiveViewport() {
const activeViewportElement = OHIF.viewerbase.viewportUtils.getActiveViewportElement();
this.enablePrefetchOnElement(activeViewportElement);
this.attachActiveViewportListeners(activeViewportElement);
}
disableViewportPrefetch() {
$('.imageViewerViewport').each(function() {
if (!$(this).find('canvas').length) {
return;
}
cornerstoneTools.stackPrefetch.disable(this);
});
}
hasStack(element) {
const stack = cornerstoneTools.getToolState(element, 'stack');
return stack && stack.data.length && (stack.data[0].imageIds.length > 1);
}
/**
* This function enables stack prefetching for a specified element (viewport)
* It first disables any prefetching currently occurring on any other viewports.
*
* @param element {node} DOM Node representing the viewport element
*/
enablePrefetchOnElement(element) {
if (!$(element).find('canvas').length) {
return;
}
// Make sure there is a stack to fetch
if (this.hasStack(element)) {
// Check if this is a clip or not
const activeViewportIndex = window.store.getState().viewports.activeViewport;
const displaySetInstanceUid = OHIF.viewer.data.loadedSeriesData[activeViewportIndex].displaySetInstanceUid;
const stack = StackManager.findStack(displaySetInstanceUid);
if (!stack) {
throw new OHIFError(`Requested stack ${displaySetInstanceUid} was not created`);
}
cornerstoneTools.stackPrefetch.enable(element);
}
}
attachActiveViewportListeners(activeViewportElement) {
function newImageHandler() {
// It needs to be called asynchronously because cornerstone does it at the same way.
// All instance urls to be prefetched will be removed again if we add them before
// Cornerstone callback (see stackPrefetch.onImageUpdated).
StudyPrefetcher.prefetchDisplaySetsAsync();
}
if (this.lastActiveViewportElement) {
this.lastActiveViewportElement.removeEventListener('cornerstonenewimage.StudyPrefetcher', newImageHandler);
}
activeViewportElement.removeEventListener('cornerstonenewimage.StudyPrefetcher', newImageHandler);
// Cornerstone will not attach an event listener if the element doesn't have a stack
if (this.hasStack(activeViewportElement)) {
activeViewportElement.addEventListener('cornerstonenewimage.StudyPrefetcher', newImageHandler);
}
this.lastActiveViewportElement = activeViewportElement;
}
prefetchDisplaySetsAsync(timeout) {
timeout = timeout || this.prefetchDisplaySetsTimeout;
clearTimeout(this.prefetchDisplaySetsHandler);
this.prefetchDisplaySetsHandler = setTimeout(() => {
this.prefetchDisplaySets();
}, timeout);
}
prefetchDisplaySets() {
let config;
if (Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.prefetch) {
config = Meteor.settings.public.prefetch;
} else {
config = {
order: 'closest',
displaySetCount: 1
};
}
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
this.prefetchImageIds(imageIds);
}
prefetchImageIds(imageIds) {
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
const requestPoolManager = cornerstoneTools.requestPoolManager;
const requestType = 'prefetch';
const preventCache = false;
const noop = () => {};
nonCachedImageIds.forEach(imageId => {
requestPoolManager.addRequest({}, imageId, requestType, preventCache, noop, noop);
});
requestPoolManager.startGrabbing();
}
getActiveViewportImage() {
const element = OHIF.viewerbase.viewportUtils.getActiveViewportElement();
if (!element) {
return;
}
const enabledElement = cornerstone.getEnabledElement(element);
const image = enabledElement.image;
return image;
}
getStudy(image) {
const studyMetadata = cornerstone.metaData.get('study', image.imageId);
return OHIF.viewer.Studies.find(study => study.studyInstanceUid === studyMetadata.studyInstanceUid);
}
getSeries(study, image) {
const seriesMetadata = cornerstone.metaData.get('series', image.imageId);
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
return studyMetadata.getSeriesByUID(seriesMetadata.seriesInstanceUid);
}
getInstance(series, image) {
const instanceMetadata = cornerstone.metaData.get('instance', image.imageId);
return series.getInstanceByUID(instanceMetadata.sopInstanceUid);
}
getActiveDisplaySet(displaySets, instance) {
return _.find(displaySets, displaySet => {
return _.some(displaySet.images, displaySetImage => {
return displaySetImage.sopInstanceUid === instance.sopInstanceUid;
});
});
}
getDisplaySetsToPrefetch(config) {
const image = this.getActiveViewportImage();
if (!image || !config || !config.displaySetCount) {
return [];
}
const study = this.getStudy(image);
const series = this.getSeries(study, image);
const instance = this.getInstance(series, image);
const displaySets = study.displaySets;
const activeDisplaySet = this.getActiveDisplaySet(displaySets, instance);
const prefetchMethodMap = {
topdown: 'getFirstDisplaySets',
downward: 'getNextDisplaySets',
closest: 'getClosestDisplaySets'
};
const prefetchOrder = config.order;
const methodName = prefetchMethodMap[prefetchOrder];
const getDisplaySets = this[methodName];
if (!getDisplaySets) {
if (prefetchOrder) {
OHIF.log.warn(`Invalid prefetch order configuration (${prefetchOrder})`);
}
return [];
}
return getDisplaySets.call(this, displaySets, activeDisplaySet, config.displaySetCount);
}
getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
const length = displaySets.length;
const selectedDisplaySets = [];
for (let i = 0; (i < length) && displaySetCount; i++) {
const displaySet = displaySets[i];
if (displaySet !== activeDisplaySet) {
selectedDisplaySets.push(displaySet);
displaySetCount--;
}
}
return selectedDisplaySets;
}
getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
const begin = activeDisplaySetIndex + 1;
const end = Math.min(begin + displaySetCount, displaySets.length);
return displaySets.slice(begin, end);
}
getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
const length = displaySets.length;
const selectedDisplaySets = [];
let left = activeDisplaySetIndex - 1;
let right = activeDisplaySetIndex + 1;
while (((left >= 0) || (right < length)) && displaySetCount) {
if (left >= 0) {
selectedDisplaySets.push(displaySets[left]);
displaySetCount--;
left--;
}
if ((right < length) && displaySetCount) {
selectedDisplaySets.push(displaySets[right]);
displaySetCount--;
right++;
}
}
return selectedDisplaySets;
}
getImageIdsFromDisplaySets(displaySets) {
let imageIds = [];
displaySets.forEach(displaySet => {
imageIds = imageIds.concat(this.getImageIdsFromDisplaySet(displaySet));
});
return imageIds;
}
getImageIdsFromDisplaySet(displaySet) {
const imageIds = [];
displaySet.images.forEach(image => {
const numFrames = image.numFrames;
if (numFrames > 1) {
for (let i = 0; i < numFrames; i++) {
let imageId = getImageId(image, i);
imageIds.push(imageId);
}
} else {
let imageId = getImageId(image);
imageIds.push(imageId);
}
});
return imageIds;
}
filterCachedImageIds(imageIds) {
return _.filter(imageIds, imageId => {
return !this.isImageCached(imageId);
});
}
isImageCached(imageId) {
const image = cornerstone.imageCache.imageCache[imageId];
return image && image.sizeInBytes;
}
cacheFullHandler() {
OHIF.log.warn('Cache full');
this.stopPrefetching();
}
}
@@ -1,498 +0,0 @@
import { OHIF } from 'meteor/ohif:core';
import { ReactiveVar } from 'meteor/reactive-var';
/**
* Constants
*/
const PROPERTY_SEPARATOR = '.';
const ORDER_ASC = 'asc';
const ORDER_DESC = 'desc';
const MIN_COUNT = 0x00000000;
const MAX_COUNT = 0x7FFFFFFF;
/**
* Class Definition
*/
export class TypeSafeCollection {
constructor() {
this._operationCount = new ReactiveVar(MIN_COUNT);
this._elementList = [];
this._handlers = Object.create(null);
}
/**
* 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);
}
_trigger(event, data) {
let handlers = this._handlers;
if (event in handlers) {
handlers = handlers[event];
if (!(handlers instanceof Array)) {
return;
}
for (let i = 0, limit = handlers.length; i < limit; ++i) {
let handler = handlers[i];
if (_isFunction(handler)) {
handler.call(null, data);
}
}
}
}
/**
* Public Methods
*/
onInsert(callback) {
if (_isFunction(callback)) {
let handlers = this._handlers.insert;
if (!(handlers instanceof Array)) {
handlers = [];
this._handlers.insert = handlers;
}
handlers.push(callback);
}
}
/**
* Update the payload associated with the given ID to be the new supplied payload.
* @param {string} id The ID of the entry that will be updated.
* @param {any} payload The element that will replace the previous payload.
* @returns {boolean} Returns true if the given ID is present in the collection, false otherwise.
*/
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;
}
/**
* Signal that the given element has been changed by notifying reactive data-source observers.
* This method is basically a means to invalidate the inernal reactive data-source.
* @param {any} payload The element that has been altered.
* @returns {boolean} Returns true if the element is present in the collection, false otherwise.
*/
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 an element in the collection. On success, the element ID (a unique string) is returned. On failure, returns null.
* A failure scenario only happens when the given payload is already present in the collection. Note that NO exceptions are thrown!
* @param {any} payload The element to be stored.
* @returns {string} The ID of the inserted element or null if the element already exists...
*/
insert(payload) {
let id = null,
found = this._elementWithPayload(payload, true);
if (!found) {
id = OHIF.utils.guid();
this._elements(true).push({ id, payload });
this._invalidate();
this._trigger('insert', { id, data: payload });
}
return id;
}
/**
* Remove all elements from the collection.
* @returns {void} No meaningful value is returned.
*/
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 elements from the collection that match the criteria given in the property map.
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
* @returns {Array} A list with all removed elements.
*/
remove(propertyMap) {
let found = this.findAllEntriesBy(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;
}
/**
* Provides the ID of the given element inside the collection.
* @param {any} payload The element being searched for.
* @returns {string} The ID of the given element or undefined if the element is not present.
*/
getElementId(payload) {
let found = this._elementWithPayload(payload);
return found && found.id;
}
/**
* Provides the position of the given element in the internal list returning -1 if the element is not present.
* @param {any} payload The element being searched for.
* @returns {number} The position of the given element in the internal list. If the element is not present -1 is returned.
*/
findById(id) {
let found = this._elementWithId(id);
return found && found.payload;
}
/**
* Provides the position of the given element in the internal list returning -1 if the element is not present.
* @param {any} payload The element being searched for.
* @returns {number} The position of the given element in the internal list. If the element is not present -1 is returned.
*/
indexOfElement(payload) {
return this._elements().indexOf(this._elementWithPayload(payload, true));
}
/**
* Provides the position of the element associated with the given ID in the internal list returning -1 if the element is not present.
* @param {string} id The index of the element.
* @returns {number} The position of the element associated with the given ID in the internal list. If the element is not present -1 is returned.
*/
indexOfId(id) {
return this._elements().indexOf(this._elementWithId(id, true));
}
/**
* Provides a list-like approach to the collection returning an element by index.
* @param {number} index The index of the element.
* @returns {any} If out of bounds, undefined is returned. Otherwise the element in the given position is returned.
*/
getElementByIndex(index) {
let found = ((this._elements())[index >= 0 ? index : -1]);
return found && found.payload;
}
/**
* Find an element by a criteria defined by the given callback function.
* Attention!!! The reactive source will not be notified if no valid callback is supplied...
* @param {function} callback A callback function which will define the search criteria. The callback
* function will be passed the collection element, its ID and its index in this very order. The callback
* shall return true when its criterea has been fulfilled.
* @returns {any} The matched element or undefined if not match was found.
*/
find(callback) {
let found;
if (_isFunction(callback)) {
found = this._elements().find((item, index) => {
return callback.call(this, item.payload, item.id, index);
});
}
return found && found.payload;
}
/**
* Find the first element that strictly matches the specified property map.
* @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 is not valid, an exception will be thrown.
* @returns {Any} The matched element or undefined if not match was found.
*/
findBy(propertyMap, options) {
let found;
if (_isObject(options)) {
// if the "options" argument is provided and is a valid object,
// it must be applied to the dataset before search...
const all = this.all(options);
if (all.length > 0) {
if (_isObject(propertyMap)) {
found = all.find(item => _compareToPropertyMapStrict(propertyMap, item));
} else {
found = all[0]; // simply extract the first element...
}
}
} else if (_isObject(propertyMap)) {
found = this._elements().find(item => _compareToPropertyMapStrict(propertyMap, item.payload));
if (found) {
found = found.payload;
}
}
return found;
}
/**
* Find all elements that strictly match the 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.
* @returns {Array} An array of entries of all elements that match the given criteria. Each set in
* in the array has the following format: [ elementData, elementId, elementIndex ].
*/
findAllEntriesBy(propertyMap) {
const found = [];
if (_isObject(propertyMap)) {
this._elements().forEach((item, index) => {
if (_compareToPropertyMapStrict(propertyMap, item.payload)) {
// Match! Add it to the found list...
found.push([ item.payload, item.id, index ]);
}
});
}
return found;
}
/**
* 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 is not valid, an exception will be thrown.
* @returns {Array} An array with all elements that match the given criteria and sorted in the specified sorting order.
*/
findAllBy(propertyMap, options) {
const found = this.findAllEntriesBy(propertyMap).map(item => item[0]); // Only payload is relevant...
if (_isObject(options)) {
if ('sort' in options) {
_sortListBy(found, options.sort);
}
}
return found;
}
/**
* Executes the supplied callback function for each element of the collection.
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
* @param {function} callback The callback function to be executed. The callback is passed the element,
* its ID and its index in this very order.
* @returns {void} Nothing is returned.
*/
forEach(callback) {
if (_isFunction(callback)) {
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 is 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 (_isObject(options)) {
if ('sort' in options) {
_sortListBy(list, options.sort);
}
}
return list;
}
}
/**
* Utility Functions
*/
/**
* Test if supplied argument is a valid object for current class purposes.
* Atention! The underscore version of this function should not be used for performance reasons.
*/
function _isObject(subject) {
return subject instanceof Object || typeof subject === 'object' && subject !== null;
}
/**
* Test if supplied argument is a valid string for current class purposes.
* Atention! The underscore version of this function should not be used for performance reasons.
*/
function _isString(subject) {
return typeof subject === 'string';
}
/**
* Test if supplied argument is a valid function for current class purposes.
* Atention! The underscore version of this function should not be used for performance reasons.
*/
function _isFunction(subject) {
return typeof subject === 'function';
}
/**
* Shortcut for Object's prototype "hasOwnProperty" method.
*/
const _hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Retrieve an object's property value by name. Composite property names (e.g., 'address.country.name') are accepted.
* @param {Object} targetObject 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, respectively);
* @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached.
*/
function _getPropertyValue(targetObject, propertyName) {
let propertyValue; // undefined (the default return value)
if (_isObject(targetObject) && _isString(propertyName)) {
const fragments = propertyName.split(PROPERTY_SEPARATOR);
const fragmentCount = fragments.length;
if (fragmentCount > 0) {
const firstFragment = fragments[0];
const remainingFragments = fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null;
propertyValue = targetObject[firstFragment];
if (remainingFragments !== null) {
propertyValue = _getPropertyValue(propertyValue, remainingFragments);
}
}
}
return propertyValue;
}
/**
* Compare a property map with a target object using strict comparison.
* @param {Object} propertyMap The property map whose properties will be used for comparison. Composite
* property names (e.g., 'address.country.name') will be tested against the "resolved" properties from the target object.
* @param {Object} targetObject The target object whose properties will be tested.
* @returns {boolean} Returns true if the properties match, false otherwise.
*/
function _compareToPropertyMapStrict(propertyMap, targetObject) {
let result = false;
// "for in" loops do not thown exceptions for invalid data types...
for (let propertyName in propertyMap) {
if (_hasOwnProperty.call(propertyMap, propertyName)) {
if (propertyMap[propertyName] !== _getPropertyValue(targetObject, propertyName)) {
result = false;
break;
} else if (result !== true) {
result = true;
}
}
}
return result;
}
/**
* 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.
*/
function _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 (_isString(property) && (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.
*/
function _sortListBy(list, specifiers) {
if (list instanceof Array && _isValidSortingSpecifier(specifiers)) {
const specifierCount = specifiers.length;
list.sort(function _sortListByCallback(a, b) { // callback name for stack traces...
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('Invalid Arguments');
}
}
@@ -1,227 +0,0 @@
import { Metadata } from './Metadata';
import { OHIFError } from '../OHIFError';
/**
* ATTENTION! This class should never depend on StudyMetadata or SeriesMetadata classes as this could
* possibly cause circular dependency issues.
*/
const UNDEFINED = 'undefined';
const NUMBER = 'number';
const STRING = 'string';
const STUDY_INSTANCE_UID = 'x0020000d';
const SERIES_INSTANCE_UID = 'x0020000e';
export class InstanceMetadata extends Metadata {
constructor(data, uid) {
super(data, uid);
// Initialize Private Properties
Object.defineProperties(this, {
_sopInstanceUID: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
writable: true,
value: null
},
_imageId: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
writable: true,
value: null
}
});
// Initialize Public Properties
this._definePublicProperties();
}
/**
* Private Methods
*/
/**
* Define Public Properties
* This method should only be called during initialization (inside the class constructor)
*/
_definePublicProperties() {
/**
* Property: this.sopInstanceUID
* Same as this.getSOPInstanceUID()
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
* sopInstanceCollection.findBy({
* sopInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
* });
*/
Object.defineProperty(this, 'sopInstanceUID', {
configurable: false,
enumerable: false,
get: function() {
return this.getSOPInstanceUID();
}
});
}
/**
* Public Methods
*/
/**
* Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getStudyInstanceUID() {
return this.getTagValue(STUDY_INSTANCE_UID, null);
}
/**
* Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getSeriesInstanceUID() {
return this.getTagValue(SERIES_INSTANCE_UID, 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.getTagValue(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.getTagValue(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.getTagValue(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;
}
/**
* @deprecated Please use getTagValue instead.
*/
getRawValue(tagOrProperty, defaultValue) {
return this.getTagValue(tagOrProperty, defaultValue);
}
/**
* This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
*/
getTagValue(tagOrProperty, defaultValue) {
/**
* Please override this method on a specialized class.
*/
throw new OHIFError('InstanceMetadata::getTagValue is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example');
}
/**
* 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
*/
throw new OHIFError('InstanceMetadata::tagExists is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example');
}
/**
* Get custom image id of a sop instance
* @return {Any} sop instance image id
*/
getImageId(frame) {
/**
* Please override this method
*/
throw new OHIFError('InstanceMetadata::getImageId is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example');
}
/**
* Static Methods
*/
/**
* 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;
}
}
@@ -1,127 +0,0 @@
/**
* Constants
*/
const STRING = 'string';
const NUMBER = 'number';
const FUNCTION = 'function';
const OBJECT = 'object';
/**
* Class Definition
*/
export class Metadata {
/**
* Constructor and Instance Methods
*/
constructor(data, uid) {
// Define the main "_data" private property as an immutable property.
// IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_data', {
configurable: false,
enumerable: false,
writable: false,
value: data
});
// Define the main "_uid" private property as an immutable property.
// IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_uid', {
configurable: false,
enumerable: false,
writable: false,
value: uid
});
// Define "_custom" properties as an immutable property.
// IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_custom', {
configurable: false,
enumerable: false,
writable: false,
value: Object.create(null)
});
}
getData() {
return this._data;
}
getDataProperty(propertyName) {
let propertyValue;
const _data = this._data;
if (_data instanceof Object || typeof _data === OBJECT && _data !== null) {
propertyValue = _data[propertyName];
}
return propertyValue;
}
/**
* Get unique object ID
*/
getObjectID() {
return this._uid;
}
/**
* Set custom attribute value
* @param {String} attribute Custom attribute name
* @param {Any} value Custom attribute value
*/
setCustomAttribute(attribute, value) {
this._custom[attribute] = value;
}
/**
* Get custom attribute value
* @param {String} attribute Custom attribute name
* @return {Any} Custom attribute value
*/
getCustomAttribute(attribute) {
return this._custom[attribute];
}
/**
* Check if a custom attribute exists
* @param {String} attribute Custom attribute name
* @return {Boolean} True if custom attribute exists or false if not
*/
customAttributeExists(attribute) {
return attribute in this._custom;
}
/**
* Set custom attributes in batch mode.
* @param {Object} attributeMap An object whose own properties will be used as custom attributes.
*/
setCustomAttributes(attributeMap) {
const _hasOwn = Object.prototype.hasOwnProperty;
const _custom = this._custom;
for (let attribute in attributeMap) {
if (_hasOwn.call(attributeMap, attribute)) {
_custom[attribute] = attributeMap[attribute];
}
}
}
/**
* 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;
}
}
@@ -1,128 +0,0 @@
# 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 &copy; 2016 nucleushealth&trade;. All rights reserved_
@@ -1,195 +0,0 @@
import { Metadata } from './Metadata';
import { InstanceMetadata } from './InstanceMetadata';
export class SeriesMetadata extends Metadata {
constructor(data, uid) {
super(data, uid);
// Initialize Private Properties
Object.defineProperties(this, {
_seriesInstanceUID: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
writable: true,
value: null
},
_instances: {
configurable: false,
enumerable: false,
writable: false,
value: []
},
_firstInstance: {
configurable: false,
enumerable: false,
writable: true,
value: null
}
});
// Initialize Public Properties
this._definePublicProperties();
}
/**
* Private Methods
*/
/**
* Define Public Properties
* This method should only be called during initialization (inside the class constructor)
*/
_definePublicProperties() {
/**
* Property: this.seriesInstanceUID
* Same as this.getSeriesInstanceUID()
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
* seriesCollection.findBy({
* seriesInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
* });
*/
Object.defineProperty(this, 'seriesInstanceUID', {
configurable: false,
enumerable: false,
get: function() {
return this.getSeriesInstanceUID();
}
});
}
/**
* Public Methods
*/
/**
* 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;
}
/**
* Get the first instance of the current series retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
*/
getFirstInstance() {
let instance = this._firstInstance;
if (!(instance instanceof InstanceMetadata)) {
instance = null;
const found = this.getInstanceByIndex(0);
if (found instanceof InstanceMetadata) {
this._firstInstance = found;
instance = found;
}
}
return instance;
}
/**
* 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);
}
/**
* Search the associated instances using the supplied callback as criteria. The callback is passed
* two arguments: instance (a InstanceMetadata instance) and index (the integer
* index of the instance within its series)
* @param {function} callback The callback function which will be invoked for each instance.
* @returns {InstanceMetadata|undefined} If an instance is found based on callback criteria it
* returns a InstanceMetadata. "undefined" is returned otherwise
*/
findInstance(callback) {
if (Metadata.isValidCallback(callback)) {
return this._instances.find((instance, index) => {
return callback.call(null, instance, index);
});
}
}
/**
* 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()
)
);
}
}
@@ -1,396 +0,0 @@
import { Metadata } from './Metadata';
import { SeriesMetadata } from './SeriesMetadata';
import { InstanceMetadata } from './InstanceMetadata';
import { ImageSet } from '../ImageSet';
import { OHIFError } from '../OHIFError';
export class StudyMetadata extends Metadata {
constructor(data, uid) {
super(data, uid);
// Initialize Private Properties
Object.defineProperties(this, {
_studyInstanceUID: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
writable: true,
value: null
},
_series: {
configurable: false,
enumerable: false,
writable: false,
value: []
},
_displaySets: {
configurable: false,
enumerable: false,
writable: false,
value: []
},
_firstSeries: {
configurable: false,
enumerable: false,
writable: true,
value: null
},
_firstInstance: {
configurable: false,
enumerable: false,
writable: true,
value: null
}
});
// Initialize Public Properties
this._definePublicProperties();
}
/**
* Private Methods
*/
/**
* Define Public Properties
* This method should only be called during initialization (inside the class constructor)
*/
_definePublicProperties() {
/**
* Property: this.studyInstanceUID
* Same as this.getStudyInstanceUID()
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
* studyCollection.findBy({
* studyInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
* });
*/
Object.defineProperty(this, 'studyInstanceUID', {
configurable: false,
enumerable: false,
get: function() {
return this.getStudyInstanceUID();
}
});
}
/**
* Public Methods
*/
/**
* Getter for displaySets
* @return {Array} Array of display set object
*/
getDisplaySets() {
return this._displaySets.slice();
}
/**
* Set display sets
* @param {Array} displaySets Array of display sets (ImageSet[])
*/
setDisplaySets(displaySets) {
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
}
/**
* Add a single display set to the list
* @param {Object} displaySet Display set object
* @returns {boolean} True on success, false on failure.
*/
addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet) {
this._displaySets.push(displaySet);
return true;
}
return false;
}
/**
* Invokes the supplied callback for each display set in the current study passing
* two arguments: display set (a ImageSet instance) and index (the integer
* index of the display set within the current study)
* @param {function} callback The callback function which will be invoked for each display set instance.
* @returns {undefined} Nothing is returned.
*/
forEachDisplaySet(callback) {
if (Metadata.isValidCallback(callback)) {
this._displaySets.forEach((displaySet, index) => {
callback.call(null, displaySet, index);
});
}
}
/**
* Search the associated display sets using the supplied callback as criteria. The callback is passed
* two arguments: display set (a ImageSet instance) and index (the integer
* index of the display set within the current study)
* @param {function} callback The callback function which will be invoked for each display set instance.
* @returns {undefined} Nothing is returned.
*/
findDisplaySet(callback) {
if (Metadata.isValidCallback(callback)) {
return this._displaySets.find((displaySet, index) => {
return callback.call(null, displaySet, index);
});
}
}
/**
* Retrieve the number of display sets within the current study.
* @returns {number} The number of display sets in the current study.
*/
getDisplaySetCount() {
return this._displaySets.length;
}
/**
* Returns the StudyInstanceUID of the current study.
*/
getStudyInstanceUID() {
return this._studyInstanceUID;
}
/**
* Getter for series
* @return {Array} Array of SeriesMetadata object
*/
getSeries() {
return this._series.slice();
}
/**
* 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);
}
/**
* It sorts the series based on display sets order. Each series must be an instance
* of SeriesMetadata and each display sets must be an instance of ImageSet.
* Useful example of usage:
* Study data provided by backend does not sort series at all and client-side
* needs series sorted by the same criteria used for sorting display sets.
*/
sortSeriesByDisplaySets() {
// Object for mapping display sets' index by seriesInstanceUid
const displaySetsMapping = {};
// Loop through each display set to create the mapping
this.forEachDisplaySet( (displaySet, index) => {
if (!(displaySet instanceof ImageSet)) {
throw new OHIFError(`StudyMetadata::sortSeriesByDisplaySets display set at index ${index} is not an instance of ImageSet`);
}
// In case of multiframe studies, just get the first index occurence
if (displaySetsMapping[displaySet.seriesInstanceUid] === void 0) {
displaySetsMapping[displaySet.seriesInstanceUid] = index;
}
});
// Clone of actual series
const actualSeries = this.getSeries();
actualSeries.forEach((series, index) => {
if (!(series instanceof SeriesMetadata)) {
throw new OHIFError(`StudyMetadata::sortSeriesByDisplaySets series at index ${index} is not an instance of SeriesMetadata`);
}
// Get the new series index
const seriesIndex = displaySetsMapping[series.getSeriesInstanceUID()];
// Update the series object with the new series position
this._series[seriesIndex] = 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 the first series of the current study retaining a consistent result across multiple calls.
* @return {SeriesMetadata} An instance of the SeriesMetadata class or null if it does not exist.
*/
getFirstSeries() {
let series = this._firstSeries;
if (!(series instanceof SeriesMetadata)) {
series = null;
const found = this.getSeriesByIndex(0);
if (found instanceof SeriesMetadata) {
this._firstSeries = found;
series = found;
}
}
return series;
}
/**
* Get the first instance of the current study retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
*/
getFirstInstance() {
let instance = this._firstInstance;
if (!(instance instanceof InstanceMetadata)) {
instance = null;
const firstSeries = this.getFirstSeries();
if (firstSeries instanceof SeriesMetadata) {
const found = firstSeries.getFirstInstance();
if (found instanceof InstanceMetadata) {
this._firstInstance = found;
instance = found;
}
}
}
return instance;
}
/**
* Search the associated series to find an specific instance using the supplied callback as criteria.
* The callback is passed two arguments: instance (a 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 instance.
* @returns {Object} Result object containing series (SeriesMetadata) and instance (InstanceMetadata)
* objects or an empty object if not found.
*/
findSeriesAndInstanceByInstance(callback) {
let result;
if (Metadata.isValidCallback(callback)) {
let instance;
const series = this._series.find(series => {
instance = series.findInstance(callback);
return instance instanceof InstanceMetadata;
});
// No series found
if (series instanceof SeriesMetadata) {
result = {
series,
instance
};
}
}
return result || {};
}
/**
* Find series by instance using the supplied callback as criteria. The callback is passed
* two arguments: instance (a InstanceMetadata instance) and index (the integer index of
* the instance within its series)
* @param {function} callback The callback function which will be invoked for each instance.
* @returns {SeriesMetadata|undefined} If a series is found based on callback criteria it
* returns a SeriesMetadata. "undefined" is returned otherwise
*/
findSeriesByInstance(callback) {
const result = this.findSeriesAndInstanceByInstance(callback);
return result.series;
}
/**
* Find an instance using the supplied callback as criteria. The callback is passed
* two arguments: instance (a InstanceMetadata instance) and index (the integer index of
* the instance within its series)
* @param {function} callback The callback function which will be invoked for each instance.
* @returns {InstanceMetadata|undefined} If an instance is found based on callback criteria it
* returns a InstanceMetadata. "undefined" is returned otherwise
*/
findInstance(callback) {
const result = this.findSeriesAndInstanceByInstance(callback);
return result.instance;
}
}
@@ -1,78 +0,0 @@
import { Metadata } from './Metadata';
import { OHIFError } from '../OHIFError';
import { DICOMTagDescriptions } from '../../DICOMTagDescriptions';
/**
* Constants
*/
const STUDY_INSTANCE_UID = 'x0020000d';
/**
* Class Definition
*/
export class StudySummary extends Metadata {
constructor(tagMap, attributeMap, uid) {
// Call the superclass constructor passing an plain object with no prototype to be used as the main "_data" attribute.
const _data = Object.create(null);
super(_data, uid);
// Initialize internal tag map if first argument is given.
if (tagMap !== void 0) {
this.addTags(tagMap);
}
// Initialize internal property map if second argument is given.
if (attributeMap !== void 0) {
this.setCustomAttributes(attributeMap);
}
}
getStudyInstanceUID() {
// This method should return null if StudyInstanceUID is not available to keep compatibility StudyMetadata API
return this.getTagValue(STUDY_INSTANCE_UID) || null;
}
/**
* Append tags to internal tag map.
* @param {Object} tagMap An object whose own properties will be used as tag values and appended to internal tag map.
*/
addTags(tagMap) {
const _hasOwn = Object.prototype.hasOwnProperty;
const _data = this._data;
for (let tag in tagMap) {
if (_hasOwn.call(tagMap, tag)) {
const description = DICOMTagDescriptions.find(tag);
// When a description is available, use its tag as internal key...
if (description) {
_data[description.tag] = tagMap[tag];
} else {
_data[tag] = tagMap[tag];
}
}
}
}
tagExists(tagName) {
const _data = this._data;
const description = DICOMTagDescriptions.find(tagName);
if (description) {
return (description.tag in _data);
}
return (tagName in _data);
}
getTagValue(tagName) {
const _data = this._data;
const description = DICOMTagDescriptions.find(tagName);
if (description) {
return _data[description.tag];
}
return _data[tagName];
}
}
@@ -1,48 +0,0 @@
export class WadoRsMetaDataBuilder {
constructor() {
this.tags = {};
}
addTag(tag, value, multi) {
this.tags[tag] = {
tag,
value,
multi
};
return this;
}
toJSON() {
const json = {};
const keys = Object.keys(this.tags);
keys.forEach(key => {
if (!this.tags.hasOwnProperty(key)) {
return;
}
const tag = this.tags[key];
const multi = !!tag.multi;
let value = tag.value;
if ((value == null) || ((value.length === 1) && (value[0] == null))) {
return;
}
if ((typeof value === 'string') && multi) {
value = value.split('\\');
}
if (!_.isArray(value)) {
value = [value];
}
json[key] = {
Value: value
};
});
return json;
}
}
@@ -1,75 +0,0 @@
export class OHIFPlugin {
// TODO: this class is still under development and will
// likely change in the near future
constructor () {
this.name = "Unnamed plugin";
this.description = "No description available";
}
// load an individual script URL
static loadScript(scriptURL, type = "text/javascript") {
return new Promise((resolve, reject) => {
const head = document.getElementsByTagName("head")[0];
const script = document.createElement("script");
script.onload = () => {
head.removeChild(script);
resolve();
};
script.onerror = reject;
script.src = scriptURL;
script.type = type;
script.async = false;
head.appendChild(script);
});
}
// reload all the dependency scripts and also
// the main plugin script url.
static reloadPlugin(plugin) {
if (plugin.scriptURLs && plugin.scriptURLs.length) {
plugin.scriptURLs.forEach(scriptURL => {
this.loadScript(scriptURL);
});
}
// TODO: Later we should probably merge script and module URLs
if (plugin.moduleURLs && plugin.moduleURLs.length) {
plugin.moduleURLs.forEach(moduleURLs => {
this.loadScript(moduleURLs, "module");
});
}
if (plugin.styleURLs && plugin.styleURLs.length) {
plugin.styleURLs.forEach(styleURLs => {
this.loadScript(styleURLs, "text/css");
});
}
let scriptURL = plugin.url;
if (plugin.allowCaching === false) {
scriptURL += "?" + performance.now();
}
const type = plugin.module === true ? 'module' : 'text/javascript'
console.warn(`Calling loadScript for ${plugin.name}`);
console.time(`loadScript ${plugin.name}`);
this.loadScript(scriptURL, type).then((script) => {
console.timeEnd(`loadScript ${plugin.name}`);
const entryPointFunction = OHIF.plugins.entryPoints[plugin.name];
if (entryPointFunction) {
entryPointFunction();
} else {
throw new Error(`No entry point found for ${plugin.name}`);
}
}, error => {
throw new Error(error);
});
}
}
@@ -1,170 +0,0 @@
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core';
import { OHIFPlugin } from "./OHIFPlugin";
export class ViewportPlugin extends OHIFPlugin {
constructor(name) {
super();
this.name = name;
this._destroyed = false;
this._setupListeners();
}
/**
* Retrieve a Display Set for a specific viewport by viewport index,
* if one is already displayed in the viewport.
*
* @static
* @param {Number} viewportIndex
* @return {undefined|ImageSet}
*/
static getDisplaySet(viewportIndex) {
// TODO: Move layoutManager from viewerbase to viewer
const { layoutManager } = OHIF.viewerbase;
const viewportData = layoutManager.viewportData[viewportIndex];
const { studyInstanceUid, displaySetInstanceUid } = viewportData;
const studyMetadata = OHIF.viewer.StudyMetadataList.findBy({ studyInstanceUID: studyInstanceUid });
return studyMetadata.findDisplaySet(displaySet => {
return displaySet.displaySetInstanceUid === displaySetInstanceUid;
});
}
/**
* Set up the viewport using the plugin.
*
* This should be implemented by the child class.
*
* @abstract
*
* @param {HTMLElement} div
* @param {ImageSet} displaySet
* @param {Object} viewportDetails
*/
setupViewport(div, displaySet, viewportDetails) {
throw new Error('You must override this method!');
}
/**
* Switch a single viewport to use the current ViewportPlugin
*
* @param {Number} viewportIndex The viewport to switch to the current plugin type
*/
setViewportToPlugin(viewportIndex) {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
const { layoutManager } = OHIF.viewerbase;
const viewportData = layoutManager.viewportData[viewportIndex];
if (viewportData.plugin === this.name) {
OHIF.log.info(`setViewportToPlugin: Viewport ${viewportIndex} already set to plugin ${this.name}`);
}
viewportData.plugin = this.name;
layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, viewportData);
}
/**
* Runs 'setupViewport' for the ViewportPlugin on all viewports which should
* be rendered by this plugin, but have not yet been initialized. Viewports
* which already contain contents are skipped.
*
* @private
*/
_initEmptyPluginViewports() {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
// Find all Viewport HTMLElements currently using this plugin
const pluginDivs = Array.from(document.querySelectorAll(`.viewport-plugin-${this.name}`));
// If there are no Viewports using this plugin, stop here
if (!pluginDivs.length) {
return;
}
const emptyPluginDivs = pluginDivs.filter(div => {
// Keep only divs owned by the plugin which have no contents
return div.innerHTML.trim() === '';
});
OHIF.log.info(`${this.name}: Initializing ${emptyPluginDivs.length} viewports`);
// Retrieve the list of all viewports, so we can figure out the viewport details
const allViewports = Array.from(document.querySelectorAll('.viewportContainer'));
const { layoutManager } = OHIF.viewerbase;
emptyPluginDivs.forEach(div => {
// Identify the Viewport index, and any display set that is currently
// hung in the viewport
const viewportIndex = allViewports.indexOf(div.parentNode);
const viewportData = layoutManager.viewportData[viewportIndex];
const displaySet = ViewportPlugin.getDisplaySet(viewportIndex);
// Use the plugin's setupViewport function to render the contents
// of this viewport.
this.setupViewport(div, viewportData, displaySet);
});
}
/**
* Listen for changes to the viewport layout which would necessitate a
* rerendering of the viewports. When this happens, re-render all viewports
* which are using this plugin.
*
* @private
*/
_setupListeners() {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
console.warn(`_setupListeners: ${this.name}`);
// TODO: Stop using Meteor's reactivity here
Tracker.autorun((computation) => {
const random = Session.get('LayoutManagerUpdated');
console.warn(`LayoutManagerUpdated: ${this.name}: ${random}`);
// Bail out if this is the first time the autorun
// executes (i.e. when it is being defined).
//
// Note: This has to be checked after the dependency on the
// Session variable above, or the reactive dependency will not
// be established.
if (computation.firstRun === true) {
return;
}
// In case we need to disable the use
// of this plugin, we can also stop the
// reactive computation by setting
// this.destroyed to true.
if (this._destroyed === true) {
computation.stop();
}
// Identify all viewports which should be
// rendered by the ViewportPlugin, and render
// them.
this._initEmptyPluginViewports();
});
}
/**
* Stop listening for changes to the viewport layout in order to
* automatically rerender viewports setup for use by this plugin.
*/
stopListeners() {
this._destroyed = true;
}
}
@@ -1,18 +0,0 @@
import { OHIF } from 'meteor/ohif:core';
import { OHIFPlugin } from './OHIFPlugin';
import { ViewportPlugin } from './ViewportPlugin';
// Each plugin registers an entry point function to be called
// when the loading is complete.
const plugins = {
OHIFPlugin,
ViewportPlugin,
entryPoints: {}
};
// TODO: When we reorganize the packages, we should figure out where to put this.
OHIF.plugins = plugins;
export default plugins;