Chore/kill yalc with fire (#629)

* init

* more shifting

* Move @babel dependencies up to workspace root

* Begin updating dependent libraries to use sync'd webpack builds w/ watches

* Commit changes before a long weekend

* Split packages into platform and extensions

* Consolidate how/where we specify file/module type entrypoints

* Make it possible to pass in extensions as App props

* Resolve viewer's modules

* Target for UMD bundle

* Add Webpack Stylus loader

* Provide WebPack build options for microscopy, vtk, and ui

* Better entrypoint for extensions

* Push changes up to switch PCs

* Fix ESM symlink build for Viewers

* Shift docs to root

* Specify additional lerna config props

* Begin providing basic readme info

* Rename example extension folder

* Shift links to bottom of doc

* First attempt at a netlify deploy preview

* Attempt to fix deploy preview build

* push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands

* Try to use global gitbook-cli

* Export node_modules path

* More agressive with modifying PATH

* Follow deploy log output advice

* try again to set bin path

* Try swapping bash with a new shell

* Try bash -l instead of exec bash

* Annd let's try again

* and again

* We may have figured it out johnny, boy

* We need to pull cornerstone-wado-image-loader files from hoisted node_modules

* Attempt to fix ticks/escapes

* Change copy syntax; try running tests on viewers from root for circleci

* Lower version to match circleci image

* Try alternative jest-canvas-mock location and version file syntax

* Run version command instead of calling node directly

* Try alternative jest transform

* Set path and enable workspaces

* Add codecov flags

* Use cpx so our copying finishes?

* Get all platform unit tests to run

* Run and report individually and in parallel

* Try once relying on codecov yaml to split w/ flags

* Add projects to split by flags

* Generate separate reports

* Trigger codecov after everything has finished running; these may not support flags

* Use recommended flags from issue comments for codecov

* Fix typo

* Remove clear flag

* Shift back to calling codecov from root

* Generate example for codecov issue

* Try fixing paths

* Simplify

* Try using codecov orb

* Bump circleci version

* Upload core and viewer

* Use full string paths

* Remove individual codecov calls

* Fix dir

* Combine files before upload

* Escape anchors

* tryfix syntax

* Escape string literal

* Combine lines to reduce path

* long paths

* Use home alias

* Fix filename

* Try to see the contents of our cat'd file

* Also upload `core`

* Try running with aliased folder

* Fix path; split PR and Merge unit tests into separate jobs

* Set default threshold

* Fix typo

* Changing to scoped package names

* Adding note

* Catch more updates

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.0
 - @ohif/extension-dicom-html@0.0.4-alpha.0
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0
 - @ohif/extension-dicom-pdf@0.0.8-alpha.0
 - @ohif/extension-vtk@0.1.4-alpha.0
 - @ohif/core@0.11.1-alpha.0
 - @ohif/i18n@0.2.3-alpha.0
 - @ohif/ui@0.2.18-alpha.0
 - @ohif/viewer@0.0.22-alpha.0

* Clean up primary readme

* Support for dev and dev:* commands

* Shift build command; satisfy default PWA build

* Remove old scripts

* Clean up UI to set webpack scripts

* Tidy up project links

* Clean duplicate code in UI project

* Update UI project's docs

* Clean up ui and i18n config

* Reduce duplicate code in `@ohif/core`

* Fix @ohif/ui versions

* bump @ohif/core versions

* Redux testkit dep

* Remove unused/duplicate config files for projects/packages

* ohif-core --> @ohif/core

* Safer publish command

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.1
 - @ohif/extension-dicom-html@0.0.4-alpha.1
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1
 - @ohif/extension-dicom-pdf@0.0.8-alpha.1
 - @ohif/extension-vtk@0.1.4-alpha.1
 - @ohif/core@0.11.1-alpha.1
 - @ohif/i18n@0.2.3-alpha.1
 - @ohif/ui@0.2.18-alpha.1
 - @ohif/viewer@0.0.22-alpha.1

* test version bump

* Add global prettier config

* chore(release): publish %s [skip ci]

 - @ohif/viewer@0.0.22-alpha.2

* test lerna publish

* Update package.jsons

* Fix build scripts

* catch changes

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.2
 - @ohif/extension-dicom-html@0.0.4-alpha.2
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2
 - @ohif/extension-dicom-pdf@0.0.8-alpha.2
 - @ohif/extension-vtk@0.1.4-alpha.2
 - @ohif/core@0.11.1-alpha.2
 - @ohif/i18n@0.2.3-alpha.2
 - @ohif/ui@0.2.18-alpha.2
 - @ohif/viewer@0.0.22-alpha.3

* fix eslint issues caused by eslint 6 and our react-app config

* Publish with canary tag

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.3
 - @ohif/extension-dicom-html@0.0.4-alpha.3
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3
 - @ohif/extension-dicom-pdf@0.0.8-alpha.3
 - @ohif/extension-vtk@0.1.4-alpha.3
 - @ohif/core@0.11.1-alpha.3
 - @ohif/i18n@0.2.3-alpha.3
 - @ohif/ui@0.2.18-alpha.3
 - @ohif/viewer@0.0.22-alpha.4

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.4
 - @ohif/extension-dicom-html@0.0.4-alpha.4
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4
 - @ohif/extension-dicom-pdf@0.0.8-alpha.4
 - @ohif/extension-vtk@0.1.4-alpha.4
 - @ohif/core@0.11.1-alpha.4
 - @ohif/i18n@0.2.3-alpha.4
 - @ohif/ui@0.2.18-alpha.4
 - @ohif/viewer@0.0.22-alpha.5

* Simplify build

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.5
 - @ohif/extension-dicom-html@0.0.4-alpha.5
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5
 - @ohif/extension-dicom-pdf@0.0.8-alpha.5
 - @ohif/extension-vtk@0.1.4-alpha.5
 - @ohif/core@0.11.1-alpha.5
 - @ohif/i18n@0.2.3-alpha.5
 - @ohif/ui@0.2.18-alpha.5
 - @ohif/viewer@0.0.22-alpha.6

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.6
 - @ohif/extension-dicom-html@0.0.4-alpha.6
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6
 - @ohif/extension-dicom-pdf@0.0.8-alpha.6
 - @ohif/extension-vtk@0.1.4-alpha.6
 - @ohif/core@0.11.1-alpha.6
 - @ohif/i18n@0.2.3-alpha.6
 - @ohif/ui@0.2.18-alpha.6
 - @ohif/viewer@0.0.22-alpha.7

* Split version and publish

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.7
 - @ohif/extension-dicom-html@0.0.4-alpha.7
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7
 - @ohif/extension-dicom-pdf@0.0.8-alpha.7
 - @ohif/extension-vtk@0.1.4-alpha.7
 - @ohif/core@0.11.1-alpha.7
 - @ohif/i18n@0.2.3-alpha.7
 - @ohif/ui@0.2.18-alpha.7
 - @ohif/viewer@0.0.22-alpha.8

* Try simplify netlify build

* react-viewerbase --> @ohif/ui

* add test for stylus loader

* Try new way to specify publish path

* Use an output folder that won't be ignored

* Try move command

* fiddle with paths

* Dig for some information

* Make sure we're in root for our commands

* Remove discovery

* Setup script-tag example

* Fix typo

* Move config folder

* Update cornerstone extension to use shared/root webpack configs

* capture changes

* Update extension webpack configs

* Don't check in docz output

* Fix babel runtime resolution issue

* Simplify ui build steps

* Push in progress

* Shift directories

* Get viewer dev path to work

* Fix viewer commonjs build

* Fix build pwa config for viewers

* Make sure our package specific command can be called from root

* Update notes

* Fix imports and misc. merge notes

* Bump wado-image-loader version

* Switch up commands to remove webworkers copy

* Fix CI config

* Fix more broken package references

* Add react-dropzone

* Persist so e2e test can use

* Fix lockfile

* Use cypress orb to simplify CI

* Fix config

* Try again

* Install yarn latest for orb

* Install yarn a different way

* Wait on correct port

* Run tests in viewer project

* Fix cypress config

* Make Cypress happier - use default location

* Simplify e2e testing

* Capture UI changes since fork

* Catch core up

* Grab latest deps after merge

* Fix broken import

* Make sure cornerstone-wado-image-loader resolves to 3+

* Note regarding orbs

* Fix refresh on 404 routes

* Fix odd error for cornerstone-wado-image-loader

* Different target for UMD

* Simplify dev script

* Try to get fancy with netlify output

* Let's do it again

* Try to fix measurementTools issue

* See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
Danny Brown authored and GitHub committed 2019-08-13 15:36:46 -04:00
1 parent cac911f08e
commit 08ff214ea6
919 files changed
+45652 -52097

No files matched your search

@@ -0,0 +1,225 @@
import { Metadata } from './Metadata';
import OHIFError from '../OHIFError.js';
/**
* ATTENTION! This class should never depend on StudyMetadata or SeriesMetadata classes as this could
* possibly cause circular dependency issues.
*/
const UNDEFINED = 'undefined';
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;
}
}
@@ -0,0 +1,127 @@
/**
* 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;
}
}
@@ -0,0 +1,124 @@
import { InstanceMetadata } from './InstanceMetadata';
import { DICOMTagDescriptions } from '../../utils/DICOMTagDescriptions.js';
import getImageId from '../../utils/getImageId.js';
export class OHIFInstanceMetadata extends InstanceMetadata {
/**
* @param {Object} Instance object.
*/
constructor(data, series, study, uid) {
super(data, uid);
this.init(series, study);
}
init(series, study) {
const instance = this.getData();
// Initialize Private Properties
Object.defineProperties(this, {
_sopInstanceUID: {
configurable: false,
enumerable: false,
writable: false,
value: instance.sopInstanceUid,
},
_study: {
configurable: false,
enumerable: false,
writable: false,
value: study,
},
_series: {
configurable: false,
enumerable: false,
writable: false,
value: series,
},
_instance: {
configurable: false,
enumerable: false,
writable: false,
value: instance,
},
_cache: {
configurable: false,
enumerable: false,
writable: false,
value: Object.create(null),
},
});
}
// Override
getTagValue(tagOrProperty, defaultValue, bypassCache) {
// check if this property has been cached...
if (tagOrProperty in this._cache && bypassCache !== true) {
return this._cache[tagOrProperty];
}
const propertyName = OHIFInstanceMetadata.getPropertyName(tagOrProperty);
// Search property value in the whole study metadata chain...
let rawValue;
if (propertyName in this._instance) {
rawValue = this._instance[propertyName];
} else if (propertyName in this._series) {
rawValue = this._series[propertyName];
} else if (propertyName in this._study) {
rawValue = this._study[propertyName];
}
if (rawValue !== void 0) {
// if rawValue value is not undefined, cache result...
this._cache[tagOrProperty] = rawValue;
return rawValue;
}
return defaultValue;
}
// Override
tagExists(tagOrProperty) {
const propertyName = OHIFInstanceMetadata.getPropertyName(tagOrProperty);
return (
propertyName in this._instance ||
propertyName in this._series ||
propertyName in this._study
);
}
// Override
getImageId(frame, thumbnail) {
// If _imageID is not cached, create it
if (this._imageId === null) {
this._imageId = getImageId(this.getData(), frame, thumbnail);
}
return this._imageId;
}
/**
* Static methods
*/
// @TODO: The current mapping of standard DICOM property names to local property names is not optimal.
// The inconsistency in property naming makes this function increasingly complex.
// A possible solution to improve this would be adapt retriveMetadata names to use DICOM standard names as in dicomTagDescriptions.js
static getPropertyName(tagOrProperty) {
let propertyName;
const tagInfo = DICOMTagDescriptions.find(tagOrProperty);
if (tagInfo !== void 0) {
// This function tries to translate standard DICOM property names into local naming convention.
propertyName = tagInfo.keyword
.replace(/^SOP/, 'sop')
.replace(/UID$/, 'Uid')
.replace(/ID$/, 'Id');
propertyName =
propertyName.charAt(0).toLowerCase() + propertyName.substr(1);
}
return propertyName;
}
}
@@ -0,0 +1,29 @@
import { SeriesMetadata } from './SeriesMetadata';
import { OHIFInstanceMetadata } from './OHIFInstanceMetadata';
export class OHIFSeriesMetadata extends SeriesMetadata {
/**
* @param {Object} Series object.
*/
constructor(data, study, uid) {
super(data, uid);
this.init(study);
}
init(study) {
const series = this.getData();
// define "_seriesInstanceUID" protected property...
Object.defineProperty(this, '_seriesInstanceUID', {
configurable: false,
enumerable: false,
writable: false,
value: series.seriesInstanceUid,
});
// populate internal list of instances...
series.instances.forEach(instance => {
this.addInstance(new OHIFInstanceMetadata(instance, series, study));
});
}
}
@@ -0,0 +1,29 @@
import { StudyMetadata } from './StudyMetadata';
import { OHIFSeriesMetadata } from './OHIFSeriesMetadata';
export class OHIFStudyMetadata extends StudyMetadata {
/**
* @param {Object} Study object.
*/
constructor(data, uid) {
super(data, uid);
this.init();
}
init() {
const study = this.getData();
// define "_studyInstanceUID" protected property
Object.defineProperty(this, '_studyInstanceUID', {
configurable: false,
enumerable: false,
writable: false,
value: study.studyInstanceUid,
});
// populate internal list of series
study.seriesList.forEach(series => {
this.addSeries(new OHIFSeriesMetadata(series, study));
});
}
}
@@ -0,0 +1,145 @@
# 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 usually
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 exclusively by the classes themselves (for internal purposes only)
were written with an initial '\_' character, being thus treated as "_protected_"
members. The idea behind this practice was never to hide them from the
programmers (what makes debugging tasks painful) but only advise for something
that's not part of the official public API and thus should not be relied on.
Usage of "protected" members makes the code less readable and prone to
compatibility issues.
As an example, the initial implementation of the `StudyMetadata` class defined
the attribute `_studyInstanceUID` and the method `getStudyInstanceUID`. This
implies that whenever the _StudyInstanceUID_ of a given study needs to be
retrieved the `getStudyInstanceUID` method should be called instead of directly
accessing the attribute `_studyInstanceUID` (which might not even be populated
since `getStudyInstanceUID` can be possiblity overriden by a subclass to satisfy
specific implementation needs, leaving the attribute `_studyInstanceUID`
unused).
Ex:
```javascript
let studyUID = myStudy.getStudyInstanceUID(); // GOOD! :-)
[ ... ]
let otherStudyUID = anotherStudy._studyInstanceUID; // BAD... :-(
```
Another important topic is the preference of _methods_ over _attributes_ on the
public API. This design decision was made to ensure extensibility and
flexibility (methods are extensible while standalone attributes are not, and can
be adapted – through overrides, for example – to support even the most peculiar
data models) even though the overhead a few additional function calls may incur.
## Abstract Classes
Some classes defined in this module are "_abstract_" classes (even though
JavaScript does not _officially_ support such programming facility). They are
_abstract_ in the sense that a few methods (very important ones, by the way)
were left "_blank_" (unimplemented, or more precisely implemented as empty NOP
functions) in order to be implemented by specialized subclasses. Methods
believed to be more generic were implemented in an attempt to satify most
implementation needs but nothing prevents a subclass from overriding them as
well (again, flexibility and extensibility are design goals). Most implemented
methods rely on the implementation of an unimplemented method. For example, the
method `getStringValue` from `InstanceMetadata` class, which has indeed been
implemented and is meant to retrieve a metadata value as a string, internally
calls the `getRawValue` method which _was NOT implemented_ and is meant to query
the internal data structures for the requested metadata value and return it _as
is_. Used in that way, an application would not benefit much from the already
implemented methods. On the other hand, by simply overriding the `getRawValue`
method on a specialized class to deal with the intrinsics of its internal data
structures, this very application would now benefit from all already implemented
methods.
The following code snippet tries to illustrate the idea:
```javascript
// -- InstanceMetadata.js
class InstanceMetadata {
[ ... ]
getRawValue(tagOrProperty, defaultValue) {
// Please implement this method in a specialized subclass...
}
[ ... ]
getStringValue(tagOrProperty, index, defaultValue) {
let rawValue = this.getRawValue(tagOrProperty, '');
// parse the returned value into a string...
[ ... ]
return stringValue;
}
[ ... ]
}
// -- MyFancyAppInstanceMetadata.js
class MyFancyAppInstanceMetadata extends InstanceMetadata {
// Overriding this method will make all methods implemented in the super class
// that rely on it to be immediately available...
getRawValue(tagOrProperty, defaultValue) {
let rawValue;
// retrieve raw value from internal data structures...
[ ... ]
return rawValue;
}
}
// -- main.js
[ ... ]
let sopInstaceMetadata = new MyFancyAppInstanceMetadata(myInternalData);
if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true
// this code will be executed...
}
if (sopInstaceMetadata instanceof InstanceMetadata) { // also true
// this code will also be executed...
}
// The following will also work since the internal "getRawValue" call inside
// "getStringValue" method will now be satisfied... (thanks to the override)
let patientName = sopInstaceMetadata.getStringValue('PatientName', '');
[ ... ]
```
_Copyright © 2016 nucleushealth™. All rights reserved_
@@ -0,0 +1,192 @@
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())
);
}
}
@@ -0,0 +1,661 @@
// - createStacks
import DICOMWeb from './../../DICOMWeb';
import ImageSet from './../ImageSet';
import { InstanceMetadata } from './InstanceMetadata';
import { Metadata } from './Metadata';
import OHIFError from '../OHIFError';
import { SeriesMetadata } from './SeriesMetadata';
// - createStacks
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from './../../utils/isImage';
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();
}
/**
* Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Metadata must represent
* imaging modalities.
*
* Furthermore, for drag/drop functionality,
* it is easiest if the stack objects also contain information about
* which study they are linked to.
*
* @param {StudyMetadata} study The study instance metadata to be used
* @returns {Array} An array of series to be placed in the Study Metadata
*/
createDisplaySets(sopClassHandlerModules) {
const study = this;
const displaySets = [];
const anyDisplaySets = study.getSeriesCount();
const anySopClassHandlerModules =
sopClassHandlerModules && sopClassHandlerModules.length > 0;
if (!anyDisplaySets) {
return displaySets;
}
// Loop through the series (SeriesMetadata)
this.forEachSeries(series => {
const anyInstances = series.getInstanceCount() > 0;
if (!anyInstances) {
return;
}
const sopClassUids = getSopClassUids(series);
if (anySopClassHandlerModules) {
const displaySet = _getDisplaySetFromSopClassModule(
sopClassHandlerModules,
series,
study,
sopClassUids
);
if (displaySet) {
displaySets.push(displaySet);
return;
}
}
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
// Search through the instances (InstanceMetadata object) of this series
// Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
if (
!isImage(instance.getRawValue('x00080016')) &&
!instance.getRawValue('x00280010')
) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute(
'studyInstanceUid',
study.getStudyInstanceUID()
);
displaySet.setAttributes({
sopClassUids,
});
displaySets.push(displaySet);
}
});
// TODO
displaySets.sort(_sortBySeriesNumber);
return displaySets;
}
/**
* 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 (an 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;
}
}
/**
*
* @typedef StudyMetadata
* @property {function} getSeriesCount - returns the number of series in the study
* @property {function} forEachSeries - function that invokes callback with each series and index
* @property {function} getStudyInstanceUID - returns the study's instance UID
*
*/
/**
* @typedef SeriesMetadata
* @property {function} getSeriesInstanceUID - returns the series's instance UID
* @property {function} getData - ???
* @property {function} forEachInstance - ???
*/
const dwc = api.DICOMwebClient;
const isMultiFrame = instance => {
// NumberOfFrames (0028,0008)
return instance.getRawValue('x00280008') > 1;
};
const makeDisplaySet = (series, instances) => {
const instance = instances[0];
const imageSet = new ImageSet(instances);
const seriesData = series.getData();
// set appropriate attributes to image set...
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID
seriesDate: seriesData.seriesDate,
seriesTime: seriesData.seriesTime,
seriesInstanceUid: series.getSeriesInstanceUID(),
seriesNumber: instance.getRawValue('x00200011'),
seriesDescription: instance.getRawValue('x0008103e'),
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: instance.getRawValue('x00080060'),
isMultiFrame: isMultiFrame(instance),
});
// Sort the images in this series if needed
const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder');
if (shallSort) {
imageSet.sortBy((a, b) => {
// Sort by InstanceNumber (0020,0013)
return (
(parseInt(a.getRawValue('x00200013', 0)) || 0) -
(parseInt(b.getRawValue('x00200013', 0)) || 0)
);
});
}
// Include the first image instance number (after sorted)
imageSet.setAttribute(
'instanceNumber',
imageSet.getImage(0).getRawValue('x00200013')
);
return imageSet;
};
const isSingleImageModality = modality => {
return modality === 'CR' || modality === 'MG' || modality === 'DX';
};
function getSopClassUids(series) {
const uniqueSopClassUidsInSeries = new Set();
series.forEachInstance(instance => {
const instanceSopClassUid = instance.getRawValue('x00080016');
uniqueSopClassUidsInSeries.add(instanceSopClassUid);
});
const sopClassUids = Array.from(uniqueSopClassUidsInSeries);
return sopClassUids;
}
/**
* @private
* @param {SeriesMetadata} series
* @param {StudyMetadata} study
* @param {string[]} sopClassUids
*/
function _getDisplaySetFromSopClassModule(
sopClassHandlerExtensions, // TODO: Update Usage
series,
study,
sopClassUids
) {
// TODO: For now only use the plugins if all instances have the same sopClassUid
if (sopClassUids.length !== 1) {
console.warn(
'getDisplaySetFromSopClassPlugin: More than one SOPClassUid in the same series is not yet supported.'
);
return;
}
const sopClassUid = sopClassUids[0];
const sopClassHandlerModules = sopClassHandlerExtensions.map(extension => {
return extension.module;
});
const handlersForSopClassUid = sopClassHandlerModules.filter(module => {
return module.sopClassUids.includes(sopClassUid);
});
// TODO: Sort by something, so we can determine which plugin to use
if (!handlersForSopClassUid || !handlersForSopClassUid.length) {
return;
}
const plugin = handlersForSopClassUid[0];
const headers = DICOMWeb.getAuthorizationHeader();
const dicomWebClient = new dwc({
url: study.getData().wadoRoot,
headers,
});
let displaySet = plugin.getDisplaySetFromSeries(
series,
study,
dicomWebClient,
headers
);
if (displaySet && !displaySet.modality) {
const instance = series.getFirstInstance();
displaySet.modality = instance.getRawValue('x00080060');
}
return displaySet;
}
/**
*
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/
function _sortBySeriesNumber(a, b) {
const seriesNumberAIsGreaterOrUndefined =
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
}
@@ -0,0 +1,73 @@
import { Metadata } from './Metadata';
import { DICOMTagDescriptions } from '../../utils/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];
}
}
@@ -0,0 +1,48 @@
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 (!Array.isArray(value)) {
value = [value];
}
json[key] = {
Value: value,
};
});
return json;
}
}
@@ -0,0 +1,35 @@
import { InstanceMetadata } from './InstanceMetadata';
import { Metadata } from './Metadata';
import { OHIFInstanceMetadata } from './OHIFInstanceMetadata';
import { OHIFSeriesMetadata } from './OHIFSeriesMetadata';
import { OHIFStudyMetadata } from './OHIFStudyMetadata';
import { SeriesMetadata } from './SeriesMetadata';
import { StudyMetadata } from './StudyMetadata';
import { StudySummary } from './StudySummary';
import { WadoRsMetaDataBuilder } from './WadoRsMetaDataBuilder';
const metadata = {
Metadata,
StudySummary,
WadoRsMetaDataBuilder,
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
OHIFStudyMetadata,
OHIFSeriesMetadata,
OHIFInstanceMetadata,
};
export {
Metadata,
StudySummary,
WadoRsMetaDataBuilder,
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
OHIFStudyMetadata,
OHIFSeriesMetadata,
OHIFInstanceMetadata,
};
export default metadata;