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