ohif-viewer/platform/core/src/classes/MetadataProvider.js
Danny Brown 08ff214ea6
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
2019-08-13 15:36:46 -04:00

413 lines
13 KiB
JavaScript

import { parsingUtils } from '../lib/parsingUtils';
const FUNCTION = 'function';
class MetadataProvider {
constructor() {
// Define the main "metadataLookup" private property as an immutable property.
Object.defineProperty(this, 'metadataLookup', {
configurable: false,
enumerable: false,
writable: false,
value: new Map(),
});
// Local reference to provider function bound to current instance.
Object.defineProperty(this, '_provider', {
configurable: false,
enumerable: false,
writable: true,
value: null,
});
}
/**
* Cornerstone Metadata provider to store image meta data
* Data from instances, series, and studies are associated with
* imageIds to facilitate usage of this information by Cornerstone's Tools
*
* e.g. the imagePlane metadata object contains instance information about
* row/column pixel spacing, patient position, and patient orientation. It
* is used in CornerstoneTools to position reference lines and orientation markers.
*
* @param {String} imageId The Cornerstone ImageId
* @param {Object} data An object containing instance, series, and study metadata
*/
addMetadata(imageId, data) {
const instanceMetadata = data.instance;
const seriesMetadata = data.series;
const studyMetadata = data.study;
const numImages = data.numImages;
const metadata = {};
metadata.frameNumber = data.frameNumber;
metadata.study = {
accessionNumber: studyMetadata.accessionNumber,
patientId: studyMetadata.patientId,
studyInstanceUid: studyMetadata.studyInstanceUid,
studyDate: studyMetadata.studyDate,
studyTime: studyMetadata.studyTime,
studyDescription: studyMetadata.studyDescription,
institutionName: studyMetadata.institutionName,
patientHistory: studyMetadata.patientHistory,
};
metadata.series = {
seriesDescription: seriesMetadata.seriesDescription,
seriesNumber: seriesMetadata.seriesNumber,
seriesDate: seriesMetadata.seriesDate,
seriesTime: seriesMetadata.seriesTime,
modality: seriesMetadata.modality,
seriesInstanceUid: seriesMetadata.seriesInstanceUid,
numImages: numImages,
};
metadata.instance = instanceMetadata;
metadata.patient = {
name: studyMetadata.patientName,
id: studyMetadata.patientId,
birthDate: studyMetadata.patientBirthDate,
sex: studyMetadata.patientSex,
age: studyMetadata.patientAge,
};
// If there is sufficient information, populate
// the imagePlane object for easier use in the Viewer
metadata.imagePlane = this.getImagePlane(instanceMetadata);
// Add the metadata to the imageId lookup object
this.metadataLookup.set(imageId, metadata);
}
/**
* Return the metadata for the given imageId
* @param {String} imageId The Cornerstone ImageId
* @returns image metadata
*/
getMetadata(imageId) {
return this.metadataLookup.get(imageId);
}
/**
* Adds a set of metadata to the Cornerstone metadata provider given a specific
* imageId, type, and dataset
*
* @param imageId
* @param type (e.g. series, instance, tagDisplay)
* @param data
*/
addSpecificMetadata(imageId, type, data) {
const metadata = {};
metadata[type] = data;
const oldMetadata = this.metadataLookup.get(imageId);
this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
}
getFromImage(image, type, tag, attrName, defaultValue) {
let value;
if (image.data) {
value = this.getFromDataSet(image.data, type, tag);
} else {
value = image.instance[attrName];
}
return value === null ? defaultValue : value;
}
getFromDataSet(dataSet, type, tag) {
if (!dataSet) {
return;
}
const fn = dataSet[type];
if (!fn) {
return;
}
return fn.call(dataSet, tag);
}
getFrameIncrementPointer(image) {
const dataSet = image.data;
let frameInstancePointer = '';
if (parsingUtils.isValidDataSet(dataSet)) {
const frameInstancePointerNames = {
x00181063: 'frameTime',
x00181065: 'frameTimeVector',
};
// (0028,0009) = Frame Increment Pointer
const frameInstancePointerTag = parsingUtils.attributeTag(
dataSet,
'x00280009'
);
frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
} else {
frameInstancePointer = image.instance.frameIncrementPointer;
}
return frameInstancePointer || '';
}
getFrameTimeVector(image) {
const dataSet = image.data;
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
return parsingUtils.floatArray(dataSet, 'x00181065');
}
return image.instance.frameTimeVector;
}
getFrameTime(image) {
const dataSet = image.data;
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
// but that case is currently not supported.
return dataSet.floatString('x00181063', -1);
}
return image.instance.frameTime;
}
/**
* Updates the related metadata for missing fields given a specified image
*
* @param image
*/
updateMetadata(image) {
const imageMetadata = this.metadataLookup.get(image.imageId);
if (!imageMetadata) {
return;
}
imageMetadata.patient.age =
imageMetadata.patient.age ||
this.getFromDataSet(image.data, 'string', 'x00101010');
imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
imageMetadata.instance.columns =
imageMetadata.instance.columns || image.columns;
imageMetadata.instance.sopClassUid =
imageMetadata.instance.sopClassUid ||
this.getFromDataSet(image.data, 'string', 'x00080016');
imageMetadata.instance.sopInstanceUid =
imageMetadata.instance.sopInstanceUid ||
this.getFromDataSet(image.data, 'string', 'x00080018');
imageMetadata.instance.pixelSpacing =
imageMetadata.instance.pixelSpacing ||
this.getFromDataSet(image.data, 'string', 'x00280030');
imageMetadata.instance.frameOfReferenceUID =
imageMetadata.instance.frameOfReferenceUID ||
this.getFromDataSet(image.data, 'string', 'x00200052');
imageMetadata.instance.imageOrientationPatient =
imageMetadata.instance.imageOrientationPatient ||
this.getFromDataSet(image.data, 'string', 'x00200037');
imageMetadata.instance.imagePositionPatient =
imageMetadata.instance.imagePositionPatient ||
this.getFromDataSet(image.data, 'string', 'x00200032');
imageMetadata.instance.sliceThickness =
imageMetadata.instance.sliceThickness ||
this.getFromDataSet(image.data, 'string', 'x00180050');
imageMetadata.instance.sliceLocation =
imageMetadata.instance.sliceLocation ||
this.getFromDataSet(image.data, 'string', 'x00201041');
imageMetadata.instance.tablePosition =
imageMetadata.instance.tablePosition ||
this.getFromDataSet(image.data, 'string', 'x00189327');
imageMetadata.instance.spacingBetweenSlices =
imageMetadata.instance.spacingBetweenSlices ||
this.getFromDataSet(image.data, 'string', 'x00180088');
imageMetadata.instance.lossyImageCompression =
imageMetadata.instance.lossyImageCompression ||
this.getFromDataSet(image.data, 'string', 'x00282110');
imageMetadata.instance.lossyImageCompressionRatio =
imageMetadata.instance.lossyImageCompressionRatio ||
this.getFromDataSet(image.data, 'string', 'x00282112');
imageMetadata.instance.frameIncrementPointer =
imageMetadata.instance.frameIncrementPointer ||
this.getFromDataSet(image.data, 'string', 'x00280009');
imageMetadata.instance.frameTime =
imageMetadata.instance.frameTime ||
this.getFromDataSet(image.data, 'string', 'x00181063');
imageMetadata.instance.frameTimeVector =
imageMetadata.instance.frameTimeVector ||
this.getFromDataSet(image.data, 'string', 'x00181065');
if (
(image.data || image.instance) &&
!imageMetadata.instance.multiframeMetadata
) {
imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
image
);
}
imageMetadata.imagePlane =
imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
}
/**
* Constructs and returns the imagePlane given the metadata instance
*
* @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
* @returns imagePlane The constructed imagePlane to be used in viewer easily
*/
getImagePlane(instance) {
if (
!instance.rows ||
!instance.columns ||
!instance.pixelSpacing ||
!instance.frameOfReferenceUID ||
!instance.imageOrientationPatient ||
!instance.imagePositionPatient
) {
return;
}
const imageOrientation = instance.imageOrientationPatient.split('\\');
const imagePosition = instance.imagePositionPatient.split('\\');
let columnPixelSpacing = 1.0;
let rowPixelSpacing = 1.0;
if (instance.pixelSpacing) {
const split = instance.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]);
}
return {
frameOfReferenceUID: instance.frameOfReferenceUID,
rows: instance.rows,
columns: instance.columns,
rowCosines: [
parseFloat(imageOrientation[0]),
parseFloat(imageOrientation[1]),
parseFloat(imageOrientation[2]),
],
columnCosines: [
parseFloat(imageOrientation[3]),
parseFloat(imageOrientation[4]),
parseFloat(imageOrientation[5]),
],
imagePositionPatient: [
parseFloat(imagePosition[0]),
parseFloat(imagePosition[1]),
parseFloat(imagePosition[2]),
],
rowPixelSpacing,
columnPixelSpacing,
};
}
/**
* This function extracts miltiframe information from a dicomParser.DataSet object.
*
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
*/
getMultiframeModuleMetadata(image) {
const imageInfo = {
isMultiframeImage: false,
frameIncrementPointer: null,
numberOfFrames: 0,
frameTime: 0,
frameTimeVector: null,
averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
};
let frameTime;
const numberOfFrames = this.getFromImage(
image,
'intString',
'x00280008',
'numberOfFrames',
-1
);
if (numberOfFrames > 0) {
// set multi-frame image indicator
imageInfo.isMultiframeImage = true;
imageInfo.numberOfFrames = numberOfFrames;
// (0028,0009) = Frame Increment Pointer
const frameIncrementPointer = this.getFrameIncrementPointer(image);
if (frameIncrementPointer === 'frameTimeVector') {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
const frameTimeVector = this.getFrameTimeVector(image);
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTimeVector = frameTimeVector;
frameTime =
frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
imageInfo.averageFrameRate = 1000 / frameTime;
}
} else if (
frameIncrementPointer === 'frameTime' ||
frameIncrementPointer === ''
) {
frameTime = this.getFrameTime(image);
if (frameTime > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTime = frameTime;
imageInfo.averageFrameRate = 1000 / frameTime;
}
}
}
return imageInfo;
}
/**
* Get a bound reference to the provider function.
*/
getProvider() {
let provider = this._provider;
if (typeof this._provider !== FUNCTION) {
provider = this.provider.bind(this);
this._provider = provider;
}
return provider;
}
/**
* Looks up metadata for Cornerstone Tools given a specified type and imageId
* A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
* are keys in the stored metadata objects.
*
* @param type
* @param imageId
* @returns {Object} Relevant metadata of the specified type
*/
provider(type, imageId) {
const imageMetadata = this.metadataLookup.get(imageId);
if (!imageMetadata) {
return;
}
if (imageMetadata.hasOwnProperty(type)) {
return imageMetadata[type];
}
}
}
export default MetadataProvider;