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:
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;
|
||||
Reference in new issue
Block a user