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,120 @@
import OHIFError from '../classes/OHIFError.js';
import metadata from '../classes/metadata/';
import { validate } from './lib/validate.js';
import { CustomAttributeRetrievalCallbacks } from './customAttributes';
/**
* Import Constants
*/
const { StudySummary, InstanceMetadata } = metadata;
/**
* Match a Metadata instance against rules using Validate.js for validation.
* @param {StudySummary|InstanceMetadata} metadataInstance Metadata instance object
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
* @return {Object} Matching Object with score and details (which rule passed or failed)
*/
const match = (metadataInstance, rules) => {
// Make sure the supplied data is valid.
if (
!(
metadataInstance instanceof StudySummary ||
metadataInstance instanceof InstanceMetadata
)
) {
throw new OHIFError(
'HPMatcher::match metadataInstance must be an instance of StudySummary or InstanceMetadata'
);
}
const options = {
format: 'grouped',
};
const details = {
passed: [],
failed: [],
};
let requiredFailed = false;
let score = 0;
rules.forEach(rule => {
const attribute = rule.attribute;
// Do not use the custom attribute from the metadataInstance since it is subject to change
if (CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
const customAttribute = CustomAttributeRetrievalCallbacks[attribute];
metadataInstance.setCustomAttribute(
attribute,
customAttribute.callback(metadataInstance)
);
}
// Format the constraint as required by Validate.js
const testConstraint = {
[attribute]: rule.constraint,
};
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
const attributeValue = metadataInstance.customAttributeExists(attribute)
? metadataInstance.getCustomAttribute(attribute)
: metadataInstance.getTagValue(attribute);
const attributeMap = {
[attribute]: attributeValue,
};
// Use Validate.js to evaluate the constraints on the specified metadataInstance
let errorMessages;
try {
errorMessages = validate(attributeMap, testConstraint, [options]);
} catch (e) {
errorMessages = ['Something went wrong during validation.', e];
}
if (!errorMessages) {
// If no errorMessages were returned, then validation passed.
// Add the rule's weight to the total score
score += parseInt(rule.weight, 10);
// Log that this rule passed in the matching details object
details.passed.push({
rule,
});
} else {
// If errorMessages were present, then validation failed
// If the rule that failed validation was Required, then
// mark that a required Rule has failed
if (rule.required) {
requiredFailed = true;
}
// Log that this rule failed in the matching details object
// and include any error messages
details.failed.push({
rule,
errorMessages,
});
}
});
// If a required Rule has failed Validation, set the matching score to zero
if (requiredFailed) {
score = 0;
}
return {
score,
details,
requiredFailed,
};
};
const HPMatcher = {
match,
};
export { HPMatcher };
@@ -0,0 +1,824 @@
import OHIFError from '../classes/OHIFError.js';
import metadata from '../classes/metadata/';
import { StudyMetadataSource } from '../classes/StudyMetadataSource.js';
import { isImage } from '../utils/isImage.js';
import { HPMatcher } from './HPMatcher.js';
import { sortByScore } from './lib/sortByScore';
import log from '../log.js';
import sortBy from '../utils/sortBy.js';
import { CustomViewportSettings } from './customViewportSettings';
import Protocol from './classes/Protocol';
import { ProtocolStore } from './protocolStore/classes';
/**
* Import Constants
*/
const { StudyMetadata, InstanceMetadata, StudySummary } = metadata;
// Useful constants
const ABSTRACT_PRIOR_VALUE = 'abstractPriorValue';
export default class ProtocolEngine {
matchedProtocols = new Map();
matchedProtocolScores = {};
/**
* Constructor
* @param {ProtocolStore} protocolStore Protocol Store used to keep track of all hanging protocols
* @param {Array} studies Array of study metadata
* @param {Map} priorStudies Map of prior studies
* @param {Object} studyMetadataSource Instance of StudyMetadataSource (ohif-viewerbase) Object to get study metadata
* @param {Object} options
*/
constructor(
protocolStore,
studies,
priorStudies,
studyMetadataSource,
options = {}
) {
// -----------
// Type Validations
if (!(studyMetadataSource instanceof StudyMetadataSource)) {
throw new OHIFError(
'ProtocolEngine::constructor studyMetadataSource is not an instance of StudyMetadataSource'
);
}
if (
!(studies instanceof Array) &&
!studies.every(study => study instanceof StudyMetadata)
) {
throw new OHIFError(
"ProtocolEngine::constructor studies is not an array or it's items are not instances of StudyMetadata"
);
}
// --------------
// Initialization
this.protocolStore = protocolStore;
this.studies = studies;
this.priorStudies = priorStudies instanceof Map ? priorStudies : new Map();
this.studyMetadataSource = studyMetadataSource;
this.options = options;
// Put protocol engine in a known state
this.reset();
// Create an array for new stage ids to be stored
// while editing a stage
this.newStageIds = [];
}
/**
* Resets the ProtocolEngine to the best match
*/
reset() {
const protocol = this.getBestProtocolMatch();
this.setHangingProtocol(protocol);
}
/**
* Retrieves the current Stage from the current Protocol and stage index
*
* @returns {*} The Stage model for the currently displayed Stage
*/
getCurrentStageModel() {
return this.protocol.stages[this.stage];
}
/**
* Finds the best protocols from Protocol Store, matching each protocol matching rules
* with the given study. The best protocol are orded by score and returned in an array
* @param {Object} study StudyMetadata instance object
* @return {Array} Array of match objects or an empty array if no match was found
* Each match object has the score of the matching and the matched
* protocol
*/
findMatchByStudy(study) {
log.trace('ProtocolEngine::findMatchByStudy');
const matched = [];
const studyInstance = study.getFirstInstance();
// Set custom attribute for study metadata
const numberOfAvailablePriors = this.getNumberOfAvailablePriors(
study.getObjectID()
);
this.protocolStore.getProtocol().forEach(protocol => {
// Clone the protocol's protocolMatchingRules array
// We clone it so that we don't accidentally add the
// numberOfPriorsReferenced rule to the Protocol itself.
let rules = protocol.protocolMatchingRules.slice();
if (!rules) {
return;
}
// Check if the study has the minimun number of priors used by the protocol.
const numberOfPriorsReferenced = protocol.getNumberOfPriorsReferenced();
if (numberOfPriorsReferenced > numberOfAvailablePriors) {
return;
}
// Run the matcher and get matching details
const matchedDetails = HPMatcher.match(studyInstance, rules);
const score = matchedDetails.score;
// The protocol matched some rule, add it to the matched list
if (score > 0) {
matched.push({
score,
protocol,
});
}
});
// If no matches were found, select the default protocol
if (!matched.length) {
const defaultProtocol = this.protocolStore.getProtocol('defaultProtocol');
return [
{
score: 1,
protocol: defaultProtocol,
},
];
}
// Sort the matched list by score
sortByScore(matched);
log.trace('ProtocolEngine::findMatchByStudy matched', matched);
return matched;
}
_clearMatchedProtocols() {
this.matchedProtocols.clear();
this.matchedProtocolScores = {};
}
/**
* Populates the MatchedProtocols Collection by running the matching procedure
*/
updateProtocolMatches() {
log.trace('ProtocolEngine::updateProtocolMatches');
// Clear all data currently in matchedProtocols
this._clearMatchedProtocols();
// For each study, find the matching protocols
this.studies.forEach(study => {
const matched = this.findMatchByStudy(study);
// For each matched protocol, check if it is already in MatchedProtocols
matched.forEach(matchedDetail => {
const protocol = matchedDetail.protocol;
if (!protocol) {
return;
}
// If it is not already in the MatchedProtocols Collection, insert it with its score
if (!this.matchedProtocols.has(protocol.id)) {
log.trace(
'ProtocolEngine::updateProtocolMatches inserting protocol match',
matchedDetail
);
this.matchedProtocols.set(protocol.id, protocol);
this.matchedProtocolScores[protocol.id] = matchedDetail.score;
}
});
});
}
_largestKeyByValue(obj) {
return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
}
_getHighestScoringProtocol() {
if (!Object.keys(this.matchedProtocolScores).length) {
return this.protocolStore.getProtocol('defaultProtocol');
}
const highestScoringProtocolId = this._largestKeyByValue(
this.matchedProtocolScores
);
return this.matchedProtocols.get(highestScoringProtocolId);
}
/**
* Return the best matched Protocol to the current study or set of studies
* @returns {*}
*/
getBestProtocolMatch() {
// Run the matching to populate matchedProtocols Set and Map
this.updateProtocolMatches();
// Retrieve the highest scoring Protocol
const bestMatch = this._getHighestScoringProtocol();
log.trace('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
return bestMatch;
}
/**
* Get the number of prior studies supplied in the priorStudies map property.
*
* @param {String} studyObjectID The study object ID of the study whose priors are needed
* @returns {number} The number of available prior studies with the same PatientID
*/
getNumberOfAvailablePriors(studyObjectID) {
return this.getAvailableStudyPriors(studyObjectID).length;
}
/**
* Get the array of prior studies from a specific study.
*
* @param {String} studyObjectID The study object ID of the study whose priors are needed
* @returns {Array} The array of available priors or an empty array
*/
getAvailableStudyPriors(studyObjectID) {
const priors = this.priorStudies.get(studyObjectID);
return priors instanceof Array ? priors : [];
}
// Match images given a list of Studies and a Viewport's image matching reqs
matchImages(viewport, viewportIndex) {
log.trace('ProtocolEngine::matchImages');
const {
studyMatchingRules,
seriesMatchingRules,
imageMatchingRules: instanceMatchingRules,
} = viewport;
const matchingScores = [];
const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
const firstInstance = currentStudy.getFirstInstance();
let highestStudyMatchingScore = 0;
let highestSeriesMatchingScore = 0;
// Set custom attribute for study metadata and it's first instance
currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
}
// Only used if study matching rules has abstract prior values defined...
let priorStudies;
studyMatchingRules.forEach(rule => {
if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
const validatorType = Object.keys(rule.constraint)[0];
const validator = Object.keys(rule.constraint[validatorType])[0];
let abstractPriorValue = rule.constraint[validatorType][validator];
abstractPriorValue = parseInt(abstractPriorValue, 10);
// TODO: Restrict or clarify validators for abstractPriorValue?
// No need to call it more than once...
if (!priorStudies) {
priorStudies = this.getAvailableStudyPriors(
currentStudy.getObjectID()
);
}
// TODO: Revisit this later: What about two studies with the same
// study date?
let priorStudy;
if (abstractPriorValue === -1) {
priorStudy = priorStudies[priorStudies.length - 1];
} else {
const studyIndex = Math.max(abstractPriorValue - 1, 0);
priorStudy = priorStudies[studyIndex];
}
// Invalid data
if (
!(priorStudy instanceof StudyMetadata) &&
!(priorStudy instanceof StudySummary)
) {
return;
}
const priorStudyObjectID = priorStudy.getObjectID();
// Check if study metadata is already in studies list
if (
this.studies.find(study => study.getObjectID() === priorStudyObjectID)
) {
return;
}
// Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
this.studyMetadataSource.loadStudy(priorStudy).then(
studyMetadata => {
// Set the custom attribute abstractPriorValue for the study metadata
studyMetadata.setCustomAttribute(
ABSTRACT_PRIOR_VALUE,
abstractPriorValue
);
// Also add custom attribute
const firstInstance = studyMetadata.getFirstInstance();
if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute(
ABSTRACT_PRIOR_VALUE,
abstractPriorValue
);
}
// Insert the new study metadata
this.studies.push(studyMetadata);
// Update the viewport to refresh layout manager with new study
this.updateViewports(viewportIndex);
},
error => {
log.warn(error);
throw new OHIFError(
`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
);
}
);
}
// TODO: Add relative Date / time
});
this.studies.forEach(study => {
const studyMatchDetails = HPMatcher.match(
study.getFirstInstance(),
studyMatchingRules
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
if (
studyMatchDetails.requiredFailed === true ||
studyMatchDetails.score < highestStudyMatchingScore
) {
return;
}
highestStudyMatchingScore = studyMatchDetails.score;
study.forEachSeries(series => {
const seriesMatchDetails = HPMatcher.match(
series.getFirstInstance(),
seriesMatchingRules
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
if (
seriesMatchDetails.requiredFailed === true ||
seriesMatchDetails.score < highestSeriesMatchingScore
) {
return;
}
highestSeriesMatchingScore = seriesMatchDetails.score;
series.forEachInstance((instance, index) => {
// This tests to make sure there is actually image data in this instance
// TODO: Change this when we add PDF and MPEG support
// See https://ohiforg.atlassian.net/browse/LT-227
// sopClassUid = x00080016
// rows = x00280010
if (
!isImage(instance.getTagValue('x00080016')) &&
!instance.getTagValue('x00280010')
) {
return;
}
const instanceMatchDetails = HPMatcher.match(
instance,
instanceMatchingRules
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
if (instanceMatchDetails.requiredFailed === true) {
return;
}
const matchDetails = {
passed: [],
failed: [],
};
matchDetails.passed = matchDetails.passed.concat(
instanceMatchDetails.details.passed
);
matchDetails.passed = matchDetails.passed.concat(
seriesMatchDetails.details.passed
);
matchDetails.passed = matchDetails.passed.concat(
studyMatchDetails.details.passed
);
matchDetails.failed = matchDetails.failed.concat(
instanceMatchDetails.details.failed
);
matchDetails.failed = matchDetails.failed.concat(
seriesMatchDetails.details.failed
);
matchDetails.failed = matchDetails.failed.concat(
studyMatchDetails.details.failed
);
const totalMatchScore =
instanceMatchDetails.score +
seriesMatchDetails.score +
studyMatchDetails.score;
const currentSOPInstanceUID = instance.getSOPInstanceUID();
const imageDetails = {
studyInstanceUid: study.getStudyInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
sopInstanceUid: currentSOPInstanceUID,
currentImageIdIndex: index,
matchingScore: totalMatchScore,
matchDetails: matchDetails,
sortingInfo: {
score: totalMatchScore,
study:
instance.getTagValue('x00080020') +
instance.getTagValue('x00080030'), // StudyDate = x00080020 StudyTime = x00080030
series: parseInt(instance.getTagValue('x00200011')), // TODO: change for seriesDateTime SeriesNumber = x00200011
instance: parseInt(instance.getTagValue('x00200013')), // TODO: change for acquisitionTime InstanceNumber = x00200013
},
};
// Find the displaySet
const displaySet = study.findDisplaySet(displaySet =>
displaySet.images.find(
image => image.getSOPInstanceUID() === currentSOPInstanceUID
)
);
// If the instance was found, set the displaySet ID
if (displaySet) {
imageDetails.displaySetInstanceUid = displaySet.getUID();
imageDetails.imageId = instance.getImageId();
}
matchingScores.push(imageDetails);
});
});
});
// Sort the matchingScores
const sortingFunction = sortBy(
{
name: 'score',
reverse: true,
},
{
name: 'study',
reverse: true,
},
{
name: 'instance',
},
{
name: 'series',
}
);
matchingScores.sort((a, b) =>
sortingFunction(a.sortingInfo, b.sortingInfo)
);
const bestMatch = matchingScores[0];
log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
return {
bestMatch,
matchingScores,
};
}
/**
* Sets the current layout
*
* @param rows
* @param columns
*/
setLayout(rows, columns) {
if (rows < 1 && columns < 1) {
log.error(`Invalid layout ${rows} x ${columns}`);
return;
}
if (typeof this.options.setLayout !== 'function') {
log.error('Hanging Protocol Engine setLayout callback is not defined');
return;
}
let viewports = [];
const numViewports = rows * columns;
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
}
this.options.setLayout({ viewports });
}
/**
* Rerenders viewports that are part of the current layout manager
* using the matching rules internal to each viewport.
*
* If this function is provided the index of a viewport, only the specified viewport
* is rerendered.
*
* @param viewportIndex
*/
updateViewports(viewportIndex) {
log.trace(
`ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
);
// Make sure we have an active protocol with a non-empty array of display sets
if (!this.getNumProtocolStages()) {
return;
}
// Retrieve the current stage
const stageModel = this.getCurrentStageModel();
// If the current stage does not fulfill the requirements to be displayed,
// stop here.
if (
!stageModel ||
!stageModel.viewportStructure ||
!stageModel.viewports ||
!stageModel.viewports.length
) {
return;
}
// Retrieve the layoutTemplate associated with the current display set's viewport structure
// If no such template name exists, stop here.
const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
if (!layoutTemplateName) {
return;
}
// Retrieve the properties associated with the current display set's viewport structure template
// If no such layout properties exist, stop here.
const layoutProps = stageModel.viewportStructure.properties;
if (!layoutProps) {
return;
}
// Create an empty array to store the output viewportData
const viewportData = [];
// Empty the matchDetails associated with the ProtocolEngine.
// This will be used to store the pass/fail details and score
// for each of the viewport matching procedures
this.matchDetails = [];
// Loop through each viewport
stageModel.viewports.forEach((viewport, viewportIndex) => {
const details = this.matchImages(viewport, viewportIndex);
this.matchDetails[viewportIndex] = details;
// Convert any YES/NO values into true/false for Cornerstone
const cornerstoneViewportParams = {};
// Cache viewportSettings keys
const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
viewportSettingsKeys.forEach(key => {
let value = viewport.viewportSettings[key];
if (value === 'YES') {
value = true;
} else if (value === 'NO') {
value = false;
}
cornerstoneViewportParams[key] = value;
});
// imageViewerViewports occasionally needs relevant layout data in order to set
// the element style of the viewport in question
const currentViewportData = {
viewportIndex,
viewport: cornerstoneViewportParams,
...layoutProps,
};
const customSettings = [];
viewportSettingsKeys.forEach(id => {
const setting = CustomViewportSettings[id];
if (!setting) {
return;
}
customSettings.push({
id: id,
value: viewport.viewportSettings[id],
});
});
currentViewportData.renderedCallback = element => {
//console.log('renderedCallback for ' + element.id);
customSettings.forEach(customSetting => {
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${
customSetting.id
}`
);
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback with value: ${
customSetting.value
}`
);
const setting = CustomViewportSettings[customSetting.id];
setting.callback(element, customSetting.value);
});
};
let currentMatch = details.bestMatch;
let currentPosition = 1;
const scoresLength = details.matchingScores.length;
while (
currentPosition < scoresLength &&
viewportData.find(a => a.imageId === currentMatch.imageId)
) {
currentMatch = details.matchingScores[currentPosition];
currentPosition++;
}
if (currentMatch && currentMatch.imageId) {
currentViewportData.studyInstanceUid = currentMatch.studyInstanceUid;
currentViewportData.seriesInstanceUid = currentMatch.seriesInstanceUid;
currentViewportData.sopInstanceUid = currentMatch.sopInstanceUid;
currentViewportData.currentImageIdIndex =
currentMatch.currentImageIdIndex;
currentViewportData.displaySetInstanceUid =
currentMatch.displaySetInstanceUid;
currentViewportData.imageId = currentMatch.imageId;
}
// @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
// if (!currentViewportData.displaySetInstanceUid) {
// throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
// }
viewportData.push(currentViewportData);
});
this.setLayout(layoutProps.rows, layoutProps.columns);
if (typeof this.options.setViewportSpecificData !== 'function') {
log.error(
'Hanging Protocol Engine setViewportSpecificData callback is not defined'
);
return;
}
// If viewportIndex is defined, then update only that viewport
if (viewportIndex !== undefined && viewportData[viewportIndex]) {
this.options.setViewportSpecificData(
viewportIndex,
viewportData[viewportIndex]
);
return;
}
// Update all viewports
viewportData.forEach(viewportSpecificData => {
this.options.setViewportSpecificData(
viewportSpecificData.viewportIndex,
viewportSpecificData
);
});
}
/**
* Sets the current Hanging Protocol to the specified Protocol
* An optional argument can also be used to prevent the updating of the Viewports
*
* @param newProtocol
* @param updateViewports
*/
setHangingProtocol(newProtocol, updateViewports = true) {
log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
log.trace(
`ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
);
// Reset the array of newStageIds
this.newStageIds = [];
if (Protocol.prototype.isPrototypeOf(newProtocol)) {
this.protocol = newProtocol;
} else {
this.protocol = new Protocol();
this.protocol.fromObject(newProtocol);
}
this.stage = 0;
// Update viewports by default
if (updateViewports) {
this.updateViewports();
}
}
/**
* Check if the next stage is available
* @return {Boolean} True if next stage is available or false otherwise
*/
isNextStageAvailable() {
const numberOfStages = this.getNumProtocolStages();
return this.stage + 1 < numberOfStages;
}
/**
* Check if the previous stage is available
* @return {Boolean} True if previous stage is available or false otherwise
*/
isPreviousStageAvailable() {
return this.stage - 1 >= 0;
}
/**
* Changes the current stage to a new stage index in the display set sequence.
* It checks if the next stage exists.
*
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
* @return {Boolean} True if new stage has set or false, otherwise
*/
setCurrentProtocolStage(stageAction) {
// Check if previous or next stage is available
if (stageAction === -1 && !this.isPreviousStageAvailable()) {
return false;
} else if (stageAction === 1 && !this.isNextStageAvailable()) {
return false;
}
// Sets the new stage
this.stage += stageAction;
// Log the new stage
log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
// Since stage has changed, we need to update the viewports
// and redo matchings
this.updateViewports();
// Everything went well
return true;
}
/**
* Retrieves the number of Stages in the current Protocol or
* undefined if no protocol or stages are set
*/
getNumProtocolStages() {
if (
!this.protocol ||
!this.protocol.stages ||
!this.protocol.stages.length
) {
return;
}
return this.protocol.stages.length;
}
/**
* Switches to the next protocol stage in the display set sequence
*/
nextProtocolStage() {
log.trace('ProtocolEngine::nextProtocolStage');
if (!this.setCurrentProtocolStage(1)) {
log.trace('ProtocolEngine::nextProtocolStage failed');
}
}
/**
* Switches to the previous protocol stage in the display set sequence
*/
previousProtocolStage() {
log.trace('ProtocolEngine::previousProtocolStage');
if (!this.setCurrentProtocolStage(-1)) {
log.trace('ProtocolEngine::previousProtocolStage failed');
}
}
}
@@ -0,0 +1,244 @@
import { ProtocolMatchingRule } from './rules';
import { removeFromArray } from '../lib/removeFromArray';
import Stage from './Stage';
import guid from '../../utils/guid';
import user from '../../user';
/**
* This class represents a Hanging Protocol at the highest level
*
* @type {Protocol}
*/
export default class Protocol {
/**
* The Constructor for the Class to create a Protocol with the bare
* minimum information
*
* @param name The desired name for the Protocol
*/
constructor(name) {
// Create a new UUID for this Protocol
this.id = guid();
// Store a value which determines whether or not a Protocol is locked
// This is probably temporary, since we will eventually have role / user
// checks for editing. For now we just need it to prevent changes to the
// default protocols.
this.locked = false;
// Boolean value to indicate if the protocol has updated priors information
// it's set in "updateNumberOfPriorsReferenced" function
this.hasUpdatedPriorsInformation = false;
// Apply the desired name
this.name = name;
// Set the created and modified dates to Now
this.createdDate = new Date();
this.modifiedDate = new Date();
// If we are logged in while creating this Protocol,
// store this information as well
if (user.userLoggedIn && user.userLoggedIn()) {
this.createdBy = user.getUserId();
this.modifiedBy = user.getUserId();
}
// Create two empty Sets specifying which roles
// have read and write access to this Protocol
this.availableTo = new Set();
this.editableBy = new Set();
// Define empty arrays for the Protocol matching rules
// and Stages
this.protocolMatchingRules = [];
this.stages = [];
// Define auxiliary values for priors
this.numberOfPriorsReferenced = -1;
}
getNumberOfPriorsReferenced(skipCache = false) {
let numberOfPriorsReferenced =
skipCache !== true ? this.numberOfPriorsReferenced : -1;
// Check if information is cached already
if (numberOfPriorsReferenced > -1) {
return numberOfPriorsReferenced;
}
numberOfPriorsReferenced = 0;
// Search each study matching rule for prior rules
// Each stage can have many viewports that can have
// multiple study matching rules.
this.stages.forEach(stage => {
if (!stage.viewports) {
return;
}
stage.viewports.forEach(viewport => {
if (!viewport.studyMatchingRules) {
return;
}
viewport.studyMatchingRules.forEach(rule => {
// If the current rule is not a priors rule, it will return -1 then numberOfPriorsReferenced will continue to be 0
const priorsReferenced = rule.getNumberOfPriorsReferenced();
if (priorsReferenced > numberOfPriorsReferenced) {
numberOfPriorsReferenced = priorsReferenced;
}
});
});
});
this.numberOfPriorsReferenced = numberOfPriorsReferenced;
return numberOfPriorsReferenced;
}
updateNumberOfPriorsReferenced() {
this.getNumberOfPriorsReferenced(true);
}
/**
* Method to update the modifiedDate when the Protocol
* has been changed
*/
protocolWasModified() {
// If we are logged in while modifying this Protocol,
// store this information as well
if (user.userLoggedIn && user.userLoggedIn()) {
this.modifiedBy = user.getUserId();
}
// Protocol has been modified, so mark priors information
// as "outdated"
this.hasUpdatedPriorsInformation = false;
// Update number of priors referenced info
this.updateNumberOfPriorsReferenced();
// Update the modifiedDate with the current Date/Time
this.modifiedDate = new Date();
}
/**
* Occasionally the Protocol class needs to be instantiated from a JavaScript Object
* containing the Protocol data. This function fills in a Protocol with the Object
* data.
*
* @param input A Protocol as a JavaScript Object, e.g. retrieved from JSON
*/
fromObject(input) {
// Check if the input already has an ID
// If so, keep it. It not, create a new UUID
this.id = input.id || guid();
// Assign the input name to the Protocol
this.name = input.name;
// Retrieve locked status, use !! to make it truthy
// so that undefined values will be set to false
this.locked = !!input.locked;
// TODO: Check how to regenerate Set from Object
//this.availableTo = new Set(input.availableTo);
//this.editableBy = new Set(input.editableBy);
// If the input contains Protocol matching rules
if (input.protocolMatchingRules) {
input.protocolMatchingRules.forEach(ruleObject => {
// Create new Rules from the stored data
var rule = new ProtocolMatchingRule();
rule.fromObject(ruleObject);
// Add them to the Protocol
this.protocolMatchingRules.push(rule);
});
}
// If the input contains data for various Stages in the
// display set sequence
if (input.stages) {
input.stages.forEach(stageObject => {
// Create Stages from the stored data
var stage = new Stage();
stage.fromObject(stageObject);
// Add them to the Protocol
this.stages.push(stage);
});
}
}
/**
* Creates a clone of the current Protocol with a new name
*
* @param name
* @returns {Protocol|*}
*/
createClone(name) {
// Create a new JavaScript independent of the current Protocol
var currentProtocol = Object.assign({}, this);
// Create a new Protocol to return
var clonedProtocol = new Protocol();
// Apply the desired properties
currentProtocol.id = clonedProtocol.id;
clonedProtocol.fromObject(currentProtocol);
// If we have specified a name, assign it
if (name) {
clonedProtocol.name = name;
}
// Unlock the clone
clonedProtocol.locked = false;
// Return the cloned Protocol
return clonedProtocol;
}
/**
* Adds a Stage to this Protocol's display set sequence
*
* @param stage
*/
addStage(stage) {
this.stages.push(stage);
// Update the modifiedDate and User that last
// modified this Protocol
this.protocolWasModified();
}
/**
* Adds a Rule to this Protocol's array of matching rules
*
* @param rule
*/
addProtocolMatchingRule(rule) {
this.protocolMatchingRules.push(rule);
// Update the modifiedDate and User that last
// modified this Protocol
this.protocolWasModified();
}
/**
* Removes a Rule from this Protocol's array of matching rules
*
* @param rule
*/
removeProtocolMatchingRule(rule) {
var wasRemoved = removeFromArray(this.protocolMatchingRules, rule);
// Update the modifiedDate and User that last
// modified this Protocol
if (wasRemoved) {
this.protocolWasModified();
}
}
}
@@ -0,0 +1,174 @@
import { comparators } from '../lib/comparators';
import guid from '../../utils/guid';
const EQUALS_REGEXP = /^equals$/;
/**
* This Class represents a Rule to be evaluated given a set of attributes
* Rules have:
* - An attribute (e.g. 'seriesDescription')
* - A constraint Object, in the form required by Validate.js:
*
* rule.constraint = {
* contains: {
* value: 'T-1'
* }
* };
*
* Note: In this example we use the 'contains' Validator, which is a custom Validator defined in Viewerbase
*
* - A value for whether or not they are Required to be matched (default: False)
* - A value for their relative weighting during Protocol or Image matching (default: 1)
*/
export default class Rule {
/**
* The Constructor for the Class to create a Rule with the bare
* minimum information
*
* @param name The desired name for the Rule
*/
constructor(attribute, constraint, required, weight) {
// Create a new UUID for this Rule
this.id = guid();
// Set the Rule's weight (defaults to 1)
this.weight = weight || 1;
// If an attribute is specified, assign it
if (attribute) {
this.attribute = attribute;
}
// If a constraint is specified, assign it
if (constraint) {
this.constraint = constraint;
}
// If a value for 'required' is specified, assign it
if (required === undefined) {
// If no value was specified, default to False
this.required = false;
} else {
this.required = required;
}
// Cache for constraint info object
this._constraintInfo = void 0;
// Cache for validator and value object
this._validatorAndValue = void 0;
}
/**
* Occasionally the Rule class needs to be instantiated from a JavaScript Object.
* This function fills in a Protocol with the Object data.
*
* @param input A Rule as a JavaScript Object, e.g. retrieved from JSON
*/
fromObject(input) {
// Check if the input already has an ID
// If so, keep it. It not, create a new UUID
this.id = input.id || guid();
// Assign the specified input data to the Rule
this.required = input.required;
this.weight = input.weight;
this.attribute = input.attribute;
this.constraint = input.constraint;
}
/**
* Get the constraint info object for the current constraint
* @return {Object\undefined} Constraint object or undefined if current constraint
* is not valid or not found in comparators list
*/
getConstraintInfo() {
let constraintInfo = this._constraintInfo;
// Check if info is cached already
if (constraintInfo !== void 0) {
return constraintInfo;
}
const ruleConstraint = Object.keys(this.constraint)[0];
if (ruleConstraint !== void 0) {
constraintInfo = comparators.find(
comparator => ruleConstraint === comparator.id
);
}
// Cache this information for later use
this._constraintInfo = constraintInfo;
return constraintInfo;
}
/**
* Check if current rule is related to priors
* @return {Boolean} True if a rule is related to priors or false otherwise
*/
isRuleForPrior() {
// @TODO: Should we check this too? this.attribute === 'relativeTime'
return this.attribute === 'abstractPriorValue';
}
/**
* If the current rule is a rule for priors, returns the number of referenced priors. Otherwise, returns -1.
* @return {Number} The number of referenced priors or -1 if not applicable. Returns zero if the actual value could not be determined.
*/
getNumberOfPriorsReferenced() {
if (!this.isRuleForPrior()) {
return -1;
}
// Get rule's validator and value
const ruleValidatorAndValue = this.getConstraintValidatorAndValue();
const { value, validator } = ruleValidatorAndValue;
const intValue = parseInt(value, 10) || 0; // avoid possible NaN
// "Equal to" validators
if (EQUALS_REGEXP.test(validator)) {
// In this case, -1 (the oldest prior) indicates that at least one study is used
return intValue < 0 ? 1 : intValue;
}
// Default cases return value
return 0;
}
/**
* Get the constraint validator and value
* @return {Object|undefined} Returns an object containing the validator and it's value or undefined
*/
getConstraintValidatorAndValue() {
let validatorAndValue = this._validatorAndValue;
// Check if validator and value are cached already
if (validatorAndValue !== void 0) {
return validatorAndValue;
}
// Get the constraint info object
const constraintInfo = this.getConstraintInfo();
// Constraint info object exists and is valid
if (constraintInfo !== void 0) {
const validator = constraintInfo.validator;
const currentValidator = this.constraint[validator];
if (currentValidator) {
const constraintValidator = constraintInfo.validatorOption;
const constraintValue = currentValidator[constraintValidator];
validatorAndValue = {
value: constraintValue,
validator: constraintInfo.id,
};
this._validatorAndValue = validatorAndValue;
}
}
return validatorAndValue;
}
}
@@ -0,0 +1,85 @@
import ViewportStructure from './ViewportStructure';
import Viewport from './Viewport';
import guid from '../../utils/guid';
/**
* A Stage is one step in the Display Set Sequence for a Hanging Protocol
*
* Stages are defined as a ViewportStructure and an array of Viewports
*
* @type {Stage}
*/
export default class Stage {
constructor(ViewportStructure, name) {
// Create a new UUID for this Stage
this.id = guid();
// Assign the name and ViewportStructure provided
this.name = name;
this.viewportStructure = ViewportStructure;
// Create an empty array for the Viewports
this.viewports = [];
// Set the created date to Now
this.createdDate = new Date();
}
/**
* Creates a clone of the current Stage with a new name
*
* @param name
* @returns {Stage|*}
*/
createClone(name) {
// Create a new JavaScript independent of the current Protocol
var currentStage = Object.assign({}, this);
// Create a new Stage to return
var clonedStage = new Stage();
// Assign the desired properties
currentStage.id = clonedStage.id;
clonedStage.fromObject(currentStage);
// If we have specified a name, assign it
if (name) {
clonedStage.name = name;
}
// Return the cloned Stage
return clonedStage;
}
/**
* Occasionally the Stage class needs to be instantiated from a JavaScript Object.
* This function fills in a Protocol with the Object data.
*
* @param input A Stage as a JavaScript Object, e.g. retrieved from JSON
*/
fromObject(input) {
// Check if the input already has an ID
// If so, keep it. It not, create a new UUID
this.id = input.id || guid();
// Assign the input name to the Stage
this.name = input.name;
// If a ViewportStructure is present in the input, add it from the
// input data
this.viewportStructure = new ViewportStructure();
this.viewportStructure.fromObject(input.viewportStructure);
// If any viewports are present in the input object
if (input.viewports) {
input.viewports.forEach(viewportObject => {
// Create a new Viewport with their data
var viewport = new Viewport();
viewport.fromObject(viewportObject);
// Add it to the viewports array
this.viewports.push(viewport);
});
}
}
}
@@ -0,0 +1,85 @@
import {
StudyMatchingRule,
SeriesMatchingRule,
ImageMatchingRule,
} from './rules';
import { removeFromArray } from '../lib/removeFromArray';
/**
* This Class defines a Viewport in the Hanging Protocol Stage. A Viewport contains
* arrays of Rules that are matched in the ProtocolEngine in order to determine which
* images should be hung.
*
* @type {Viewport}
*/
export default class Viewport {
constructor() {
this.viewportSettings = {};
this.imageMatchingRules = [];
this.seriesMatchingRules = [];
this.studyMatchingRules = [];
}
/**
* Occasionally the Viewport class needs to be instantiated from a JavaScript Object.
* This function fills in a Viewport with the Object data.
*
* @param input The Viewport as a JavaScript Object, e.g. retrieved from JSON
*/
fromObject(input) {
// If ImageMatchingRules exist, create them from the Object data
// and add them to the Viewport's imageMatchingRules array
if (input.imageMatchingRules) {
input.imageMatchingRules.forEach(ruleObject => {
var rule = new ImageMatchingRule();
rule.fromObject(ruleObject);
this.imageMatchingRules.push(rule);
});
}
// If SeriesMatchingRules exist, create them from the Object data
// and add them to the Viewport's seriesMatchingRules array
if (input.seriesMatchingRules) {
input.seriesMatchingRules.forEach(ruleObject => {
var rule = new SeriesMatchingRule();
rule.fromObject(ruleObject);
this.seriesMatchingRules.push(rule);
});
}
// If StudyMatchingRules exist, create them from the Object data
// and add them to the Viewport's studyMatchingRules array
if (input.studyMatchingRules) {
input.studyMatchingRules.forEach(ruleObject => {
var rule = new StudyMatchingRule();
rule.fromObject(ruleObject);
this.studyMatchingRules.push(rule);
});
}
// If ViewportSettings exist, add them to the current protocol
if (input.viewportSettings) {
this.viewportSettings = input.viewportSettings;
}
}
/**
* Finds and removes a rule from whichever array it exists in.
* It is not required to specify if it exists in studyMatchingRules,
* seriesMatchingRules, or imageMatchingRules
*
* @param rule
*/
removeRule(rule) {
var array;
if (rule instanceof StudyMatchingRule) {
array = this.studyMatchingRules;
} else if (rule instanceof SeriesMatchingRule) {
array = this.seriesMatchingRules;
} else if (rule instanceof ImageMatchingRule) {
array = this.imageMatchingRules;
}
removeFromArray(array, rule);
}
}
@@ -0,0 +1,53 @@
/**
* The ViewportStructure class represents the layout and layout properties that
* Viewports are displayed in. ViewportStructure has a type, which corresponds to
* a layout template, and a set of properties, which depend on the type.
*
* @type {ViewportStructure}
*/
export default class ViewportStructure {
constructor(type, properties) {
this.type = type;
this.properties = properties;
}
/**
* Occasionally the ViewportStructure class needs to be instantiated from a JavaScript Object.
* This function fills in a ViewportStructure with the Object data.
*
* @param input The ViewportStructure as a JavaScript Object, e.g. retrieved from JSON
*/
fromObject(input) {
this.type = input.type;
this.properties = input.properties;
}
/**
* Retrieve the layout template name based on the layout type
*
* @returns {string}
*/
getLayoutTemplateName() {
// Viewport structure can be updated later when we build more complex display layouts
switch (this.type) {
case 'grid':
return 'gridLayout';
}
}
/**
* Retrieve the number of Viewports required for this layout
* given the layout type and properties
*
* @returns {string}
*/
getNumViewports() {
// Viewport structure can be updated later when we build more complex display layouts
switch (this.type) {
case 'grid':
// For the typical grid layout, we only need to multiply rows by columns to
// obtain the number of viewports
return this.properties.rows * this.properties.columns;
}
}
}
@@ -0,0 +1,23 @@
import Protocol from './Protocol.js';
import Rule from './Rule.js';
import Stage from './Stage.js';
import Viewport from './Viewport.js';
import ViewportStructure from './ViewportStructure.js';
import {
ProtocolMatchingRule,
StudyMatchingRule,
SeriesMatchingRule,
ImageMatchingRule,
} from './rules.js';
export {
Protocol,
Rule,
Stage,
Viewport,
ViewportStructure,
ProtocolMatchingRule,
StudyMatchingRule,
SeriesMatchingRule,
ImageMatchingRule,
};
@@ -0,0 +1,40 @@
import Rule from './Rule';
/**
* The ProtocolMatchingRule Class extends the Rule Class.
*
* At present it does not add any new methods or attributes
* @type {ProtocolMatchingRule}
*/
class ProtocolMatchingRule extends Rule {}
/**
* The StudyMatchingRule Class extends the Rule Class.
*
* At present it does not add any new methods or attributes
* @type {StudyMatchingRule}
*/
class StudyMatchingRule extends Rule {}
/**
* The SeriesMatchingRule Class extends the Rule Class.
*
* At present it does not add any new methods or attributes
* @type {SeriesMatchingRule}
*/
class SeriesMatchingRule extends Rule {}
/**
* The ImageMatchingRule class extends the Rule Class.
*
* At present it does not add any new methods or attributes
* @type {ImageMatchingRule}
*/
class ImageMatchingRule extends Rule {}
export {
ProtocolMatchingRule,
StudyMatchingRule,
SeriesMatchingRule,
ImageMatchingRule,
};
@@ -0,0 +1,30 @@
// Define an empty object to store callbacks that are used to retrieve custom attributes
// The simplest example for a custom attribute is the Timepoint type (i.e. baseline or follow-up)
// used in the LesionTracker application.
//
// Timepoint type can be obtained given a studyId, and this is done through a custom callback.
// Developers can define attributes (i.e. attributeId = timepointType) with a name ('Timepoint Type')
// and a callback function that is used to calculate them.
//
// The input to the callback, which is called during viewport-image matching rule evaluation
// is the set of attributes that contains the specified attribute. In our example, timepointType is
// linked to the study attributes, and so the inputs to the callback is an object containing
// the study attributes.
const CustomAttributeRetrievalCallbacks = {};
/**
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
* callback that will be used to calculate the attribute value.
*
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
*/
function addCustomAttribute(attributeId, attributeName, callback) {
CustomAttributeRetrievalCallbacks[attributeId] = {
name: attributeName,
callback: callback,
};
}
export { CustomAttributeRetrievalCallbacks, addCustomAttribute };
@@ -0,0 +1,22 @@
// Define an empty object to store callbacks that are used to apply custom viewport settings
// after a viewport is rendered.
const CustomViewportSettings = {};
/**
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
*
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
* @param options
* @param callback A function to be run after a viewport is rendered with a series
*/
function addCustomViewportSetting(settingId, settingName, options, callback) {
CustomViewportSettings[settingId] = {
id: settingId,
text: settingName,
options: options,
callback: callback,
};
}
export { CustomViewportSettings, addCustomViewportSetting };
@@ -0,0 +1,133 @@
export const attributeDefaults = {
abstractPriorValue: 0,
};
export const displaySettings = {
invert: {
id: 'invert',
text: 'Show Grayscale Inverted',
defaultValue: 'NO',
options: ['YES', 'NO'],
},
};
// @TODO Fix abstractPriorValue comparison
export const studyAttributes = [
{
id: 'x00100020',
text: '(x00100020) Patient ID',
},
{
id: 'x0020000d',
text: '(x0020000d) Study Instance UID',
},
{
id: 'x00080020',
text: '(x00080020) Study Date',
},
{
id: 'x00080030',
text: '(x00080030) Study Time',
},
{
id: 'x00081030',
text: '(x00081030) Study Description',
},
{
id: 'abstractPriorValue',
text: 'Abstract Prior Value',
},
];
export const protocolAttributes = [
{
id: 'x00100020',
text: '(x00100020) Patient ID',
},
{
id: 'x0020000d',
text: '(x0020000d) Study Instance UID',
},
{
id: 'x00080020',
text: '(x00080020) Study Date',
},
{
id: 'x00080030',
text: '(x00080030) Study Time',
},
{
id: 'x00081030',
text: '(x00081030) Study Description',
},
{
id: 'anatomicRegion',
text: 'Anatomic Region',
},
];
export const seriesAttributes = [
{
id: 'x0020000e',
text: '(x0020000e) Series Instance UID',
},
{
id: 'x00080060',
text: '(x00080060) Modality',
},
{
id: 'x00200011',
text: '(x00200011) Series Number',
},
{
id: 'x0008103e',
text: '(x0008103e) Series Description',
},
{
id: 'numImages',
text: 'Number of Images',
},
];
export const instanceAttributes = [
{
id: 'x00080016',
text: '(x00080016) SOP Class UID',
},
{
id: 'x00080018',
text: '(x00080018) SOP Instance UID',
},
{
id: 'x00185101',
text: '(x00185101) View Position',
},
{
id: 'x00200013',
text: '(x00200013) Instance Number',
},
{
id: 'x00080008',
text: '(x00080008) Image Type',
},
{
id: 'x00181063',
text: '(x00181063) Frame Time',
},
{
id: 'x00200060',
text: '(x00200060) Laterality',
},
{
id: 'x00541330',
text: '(x00541330) Image Index',
},
{
id: 'x00280004',
text: '(x00280004) Photometric Interpretation',
},
{
id: 'x00180050',
text: '(x00180050) Slice Thickness',
},
];
@@ -0,0 +1,14 @@
import ProtocolEngine from './ProtocolEngine.js';
import { ProtocolStore, ProtocolStrategy } from './protocolStore';
import { addCustomAttribute } from './customAttributes';
import { addCustomViewportSetting } from './customViewportSettings';
const hangingProtocols = {
ProtocolEngine,
ProtocolStore,
ProtocolStrategy,
addCustomAttribute,
addCustomViewportSetting,
};
export default hangingProtocols;
@@ -0,0 +1,98 @@
const comparators = [
{
id: 'equals',
name: '= (Equals)',
validator: 'equals',
validatorOption: 'value',
description: 'The attribute must equal this value.',
},
{
id: 'doesNotEqual',
name: '!= (Does not equal)',
validator: 'doesNotEqual',
validatorOption: 'value',
description: 'The attribute must not equal this value.',
},
{
id: 'contains',
name: 'Contains',
validator: 'contains',
validatorOption: 'value',
description: 'The attribute must contain this value.',
},
{
id: 'doesNotContain',
name: 'Does not contain',
validator: 'doesNotContain',
validatorOption: 'value',
description: 'The attribute must not contain this value.',
},
{
id: 'startsWith',
name: 'Starts with',
validator: 'startsWith',
validatorOption: 'value',
description: 'The attribute must start with this value.',
},
{
id: 'endsWith',
name: 'Ends with',
validator: 'endsWith',
validatorOption: 'value',
description: 'The attribute must end with this value.',
},
{
id: 'onlyInteger',
name: 'Only Integers',
validator: 'numericality',
validatorOption: 'onlyInteger',
description: "Real numbers won't be allowed.",
},
{
id: 'greaterThan',
name: '> (Greater than)',
validator: 'numericality',
validatorOption: 'greaterThan',
description: 'The attribute has to be greater than this value.',
},
{
id: 'greaterThanOrEqualTo',
name: '>= (Greater than or equal to)',
validator: 'numericality',
validatorOption: 'greaterThanOrEqualTo',
description: 'The attribute has to be at least this value.',
},
{
id: 'lessThanOrEqualTo',
name: '<= (Less than or equal to)',
validator: 'numericality',
validatorOption: 'lessThanOrEqualTo',
description: 'The attribute can be this value at the most.',
},
{
id: 'lessThan',
name: '< (Less than)',
validator: 'numericality',
validatorOption: 'lessThan',
description: 'The attribute has to be less than this value.',
},
{
id: 'odd',
name: 'Odd',
validator: 'numericality',
validatorOption: 'odd',
description: 'The attribute has to be odd.',
},
{
id: 'even',
name: 'Even',
validator: 'numericality',
validatorOption: 'even',
description: 'The attribute has to be even.',
},
];
// Immutable object
Object.freeze(comparators);
export { comparators };
@@ -0,0 +1,71 @@
const attributeCache = Object.create(null);
const REGEXP = /^\([x0-9a-f]+\)/;
const humanize = text => {
let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
humanized = humanized.replace(/^./, str => {
// uppercase the first character
return str.toUpperCase();
});
return humanized;
};
/**
* Get the text of an attribute for a given attribute
* @param {String} attributeId The attribute ID
* @param {Array} attributes Array of attributes objects with id and text properties
* @return {String} If found return the attribute text or an empty string otherwise
*/
const getAttributeText = (attributeId, attributes) => {
// If the attribute is already in the cache, return it
if (attributeId in attributeCache) {
return attributeCache[attributeId];
}
// Find the attribute with given attributeId
const attribute = attributes.find(attribute => attribute.id === attributeId);
let attributeText;
// If attribute was found get its text and save it on the cache
if (attribute) {
attributeText = attribute.text.replace(REGEXP, '');
attributeCache[attributeId] = attributeText;
}
return attributeText || '';
};
function displayConstraint(attributeId, constraint, attributes) {
if (!constraint || !attributeId) {
return;
}
const validatorType = Object.keys(constraint)[0];
if (!validatorType) {
return;
}
const validator = Object.keys(constraint[validatorType])[0];
if (!validator) {
return;
}
const value = constraint[validatorType][validator];
if (value === void 0) {
return;
}
let comparator = validator;
if (validator === 'value') {
comparator = validatorType;
}
const attributeText = getAttributeText(attributeId, attributes);
const constraintText =
attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
return constraintText;
}
@@ -0,0 +1,32 @@
/**
* Removes the first instance of an element from an array, if an equal value exists
*
* @param array
* @param input
*
* @returns {boolean} Whether or not the element was found and removed
*/
const removeFromArray = (array, input) => {
// If the array is empty, stop here
if (!array || !array.length) {
return false;
}
array.forEach((value, index) => {
// TODO: Double check whether or not this deep equality check is necessary
//if (_.isEqual(value, input)) {
if (value === input) {
indexToRemove = index;
return false;
}
});
if (indexToRemove === void 0) {
return false;
}
array.splice(indexToRemove, 1);
return true;
};
export { removeFromArray };
@@ -0,0 +1,8 @@
// Sorts an array by score
const sortByScore = arr => {
arr.sort((a, b) => {
return b.score - a.score;
});
};
export { sortByScore };
@@ -0,0 +1,39 @@
import validate from 'validate.js';
validate.validators.equals = function(value, options, key, attributes) {
if (options && value !== options.value) {
return key + 'must equal ' + options.value;
}
};
validate.validators.doesNotEqual = function(value, options, key) {
if (options && value === options.value) {
return key + 'cannot equal ' + options.value;
}
};
validate.validators.contains = function(value, options, key) {
if (options && value.indexOf && value.indexOf(options.value) === -1) {
return key + 'must contain ' + options.value;
}
};
validate.validators.doesNotContain = function(value, options, key) {
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
return key + 'cannot contain ' + options.value;
}
};
validate.validators.startsWith = function(value, options, key) {
if (options && value.startsWith && !value.startsWith(options.value)) {
return key + 'must start with ' + options.value;
}
};
validate.validators.endsWith = function(value, options, key) {
if (options && value.endsWith && !value.endsWith(options.value)) {
return key + 'must end with ' + options.value;
}
};
export { validate };
@@ -0,0 +1,97 @@
import Protocol from '../../classes/Protocol';
// The ProtocolStore class allows persisting hanging protocols using different strategies.
// For example, one strategy stores hanging protocols in the application server while
// another strategy stores them in a remote machine, but only one strategy can be used at a time.
export default class ProtocolStore {
constructor(strategy) {
this.strategy = strategy;
}
/**
* Get a Protocol instance or array of Protocol instances for the given protocol object or array
* @param {Object|array} protocolObject Protocol plain object or array of Protocol plain objects
* @return {Protocol|array} Protocol instance or array of Protocol intances for the given protocol object or array
*/
static getProtocolInstance(protocolObject) {
let result = protocolObject;
// If result is an array of protocols objects
if (result instanceof Array) {
result.forEach((protocol, index) => {
// Check if protocol is an instance of Protocol
if (!(protocol instanceof Protocol)) {
const protocolInstance = new Protocol();
protocolInstance.fromObject(protocol);
result[index] = protocolInstance;
}
});
} else if (result !== void 0 && !(result instanceof Protocol)) {
// Check if result exists and is not an instance of Protocol
const protocolInstance = new Protocol();
protocolInstance.fromObject(result);
result = protocolInstance;
}
return result;
}
/**
* Registers a function to be called when the protocol store is ready to persist hanging protocols
*
* NOTE: Strategies should implement this function
*
* @param callback The function to be called as a callback
*/
onReady(callback) {
this.strategy.onReady(callback);
}
/**
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to find the hanging protocol
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
*/
getProtocol(protocolId) {
let result = this.strategy.getProtocol(protocolId);
return ProtocolStore.getProtocolInstance(result);
}
/**
* Stores the hanging protocol
*
* NOTE: Strategies should implement this function
*
* @param protocol The hanging protocol to be stored
*/
addProtocol(protocol) {
this.strategy.addProtocol(protocol);
}
/**
* Updates the hanging protocol by protocolId
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to find the hanging protocol to update
* @param protocol The updated hanging protocol
*/
updateProtocol(protocolId, protocol) {
this.strategy.updateProtocol(protocolId, protocol);
}
/**
* Removes the hanging protocol
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to remove the hanging protocol
*/
removeProtocol(protocolId) {
this.strategy.removeProtocol(protocolId);
}
}
@@ -0,0 +1,77 @@
import log from '../../../log';
import defaultProtocol from '../defaultProtocol';
export default class ProtocolStrategy {
constructor() {
this.hangingProtocols = new Map();
this.defaultsAdded = false;
}
/**
* Registers a function to be called when the hangingProtocols collection is subscribed
* The callback is called only one time when the subscription is ready
*
* @param callback The function to be called as a callback
*/
onReady(callback) {
if (!this.defaultsAdded) {
log.info('Inserting the default hanging protocol...');
this.addProtocol(defaultProtocol);
this.defaultsAdded = true;
}
callback();
}
/**
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
*
* @param protocolId The protocol ID used to find the hanging protocol
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
*/
getProtocol(protocolId) {
// Return the hanging protocol by protocolId if defined
if (protocolId) {
return this.hangingProtocols.get(protocolId);
}
// Otherwise, return all protocols
return Array.from(this.hangingProtocols.values());
}
/**
* Stores the hanging protocol
*
* @param protocol The hanging protocol to be stored
*/
addProtocol(protocol) {
this.hangingProtocols.set(protocol.id, protocol);
}
/**
* Updates the hanging protocol by protocolId
*
* @param protocolId The protocol ID used to find the hanging protocol to update
* @param protocol The updated hanging protocol
*/
updateProtocol(protocolId, protocol) {
if (!this.hangingProtocols.has(protocolId)) {
return;
}
this.hangingProtocols.set(protocolId, protocol);
}
/**
* Removes the hanging protocol
*
* @param protocolId The protocol ID used to remove the hanging protocol
*/
removeProtocol(protocolId) {
if (!this.hangingProtocols.has(protocolId)) {
return;
}
this.hangingProtocols.delete(protocolId);
}
}
@@ -0,0 +1,4 @@
import ProtocolStore from './ProtocolStore';
import ProtocolStrategy from './ProtocolStrategy';
export { ProtocolStore, ProtocolStrategy };
@@ -0,0 +1,27 @@
import Protocol from '../classes/Protocol';
import ViewportStructure from '../classes/ViewportStructure';
import Viewport from '../classes/Viewport';
import Stage from '../classes/Stage';
function getDefaultProtocol() {
const protocol = new Protocol('Default');
protocol.id = 'defaultProtocol';
protocol.locked = true;
const oneByOne = new ViewportStructure('grid', {
rows: 1,
columns: 1,
});
const viewport = new Viewport();
const first = new Stage(oneByOne, 'oneByOne');
first.viewports.push(viewport);
protocol.stages.push(first);
return protocol;
}
const defaultProtocol = getDefaultProtocol();
export default defaultProtocol;
@@ -0,0 +1,5 @@
import { ProtocolStore, ProtocolStrategy } from './classes';
import defaultProtocol from './defaultProtocol';
import testProtocols from './testProtocols';
export { ProtocolStore, ProtocolStrategy, defaultProtocol, testProtocols };
File diff suppressed because it is too large. Load diff