feat: 🎸 MeasurementService (#1314)
* feat: 🎸 MeasurementService Work in progress... * Remove cornerstone tools import * Second iteration * CR Update: Add update / added events example * Add new props to measurements * Update event log * Add new measurementid to annotation * Add context support * Add value types * Add area * Add todo * Wip measurement to annotation map * Change points representation * Add props to annotation mapping * Add tests * Extract formatter from init and add tests * Sketch matchers * Fix events and valuetypes imports * Remove context support * Rename formatter to mappings * Sketching source and source definitions * Adjust matching criteria in addOrUpdate * CR Updates: Extract private functions and rename variables * Fix broken tests * Add more measurement service tests * Update broken mapping tests * Update test description * Update getAnnotation to get mapping based on def and source Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
1 parent
e8d56c11f0
commit
0c37a406d9
14 files changed
+1255
-6
No files matched your search
@@ -24,6 +24,7 @@ import {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
@@ -55,6 +56,7 @@ const OHIF = {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -85,6 +87,7 @@ export {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('Top level exports', () => {
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'MeasurementService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import log from '../../log';
|
||||
import guid from '../../utils/guid';
|
||||
|
||||
/**
|
||||
* Measurement source schema
|
||||
*
|
||||
* @typedef {Object} MeasurementSource
|
||||
* @property {number} id -
|
||||
* @property {string} name -
|
||||
* @property {string} version -
|
||||
*/
|
||||
|
||||
/**
|
||||
* Measurement schema
|
||||
*
|
||||
* @typedef {Object} Measurement
|
||||
* @property {number} id -
|
||||
* @property {string} sopInstanceUID -
|
||||
* @property {string} frameOfReferenceUID -
|
||||
* @property {string} referenceSeriesUID -
|
||||
* @property {string} label -
|
||||
* @property {string} description -
|
||||
* @property {string} type -
|
||||
* @property {string} unit -
|
||||
* @property {number} area -
|
||||
* @property {Array} points -
|
||||
* @property {MeasurementSource} source -
|
||||
*/
|
||||
|
||||
/* Measurement schema keys for object validation. */
|
||||
const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'id',
|
||||
'sopInstanceUID',
|
||||
'frameOfReferenceUID',
|
||||
'referenceSeriesUID',
|
||||
'label',
|
||||
'description',
|
||||
'type',
|
||||
'unit',
|
||||
'area', // TODO: Add concept names instead (descriptor)
|
||||
'points',
|
||||
'source',
|
||||
];
|
||||
|
||||
const EVENTS = {
|
||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||
};
|
||||
|
||||
const VALUE_TYPES = {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
};
|
||||
|
||||
class MeasurementService {
|
||||
constructor() {
|
||||
this.sources = {};
|
||||
this.mappings = {};
|
||||
this.measurements = {};
|
||||
this.listeners = {};
|
||||
Object.defineProperty(this, 'EVENTS', {
|
||||
value: EVENTS,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
Object.defineProperty(this, 'VALUE_TYPES', {
|
||||
value: VALUE_TYPES,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all measurements.
|
||||
*
|
||||
* @return {Measurement[]} Array of measurements
|
||||
*/
|
||||
getMeasurements() {
|
||||
const measurements = this._arrayOfObjects(this.measurements);
|
||||
return (
|
||||
measurements &&
|
||||
measurements.map(m => this.measurements[Object.keys(m)[0]])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific measurement by its id.
|
||||
*
|
||||
* @param {string} id If of the measurement
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
getMeasurement(id) {
|
||||
let measurement = null;
|
||||
const measurements = this.measurements[id];
|
||||
|
||||
if (measurements && Object.keys(measurements).length > 0) {
|
||||
measurement = this.measurements[id];
|
||||
}
|
||||
|
||||
return measurement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new source.
|
||||
*
|
||||
* @param {string} name Name of the source
|
||||
* @param {string} version Source name
|
||||
* @return {MeasurementSource} Measurement source instance
|
||||
*/
|
||||
createSource(name, version) {
|
||||
if (!name) {
|
||||
log.warn('Source name not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
log.warn('Source version not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = guid();
|
||||
const source = {
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
};
|
||||
source.addOrUpdate = (definition, measurement) => {
|
||||
return this.addOrUpdate(source, definition, measurement);
|
||||
};
|
||||
source.getAnnotation = (definition, measurementId) => {
|
||||
return this.getAnnotation(source, definition, measurementId);
|
||||
};
|
||||
|
||||
log.info(`New '${name}@${version}' source added.`);
|
||||
this.sources[id] = source;
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new measurement matching criteria along with mapping functions.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition Definition of the measurement (Annotation Type)
|
||||
* @param {MatchingCriteria} matchingCriteria The matching criteria
|
||||
* @param {Function} toSourceSchema Mapping function to source schema
|
||||
* @param {Function} toMeasurementSchema Mapping function to measurement schema
|
||||
* @return void
|
||||
*/
|
||||
addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
toMeasurementSchema
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchingCriteria) {
|
||||
log.warn('Matching criteria not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
log.warn('Definition not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toSourceSchema) {
|
||||
log.warn('Source mapping function not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toMeasurementSchema) {
|
||||
log.warn('Measurement mapping function not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
matchingCriteria,
|
||||
definition,
|
||||
toSourceSchema,
|
||||
toMeasurementSchema,
|
||||
};
|
||||
|
||||
if (Array.isArray(this.mappings[source.id])) {
|
||||
this.mappings[source.id].push(mapping);
|
||||
} else {
|
||||
this.mappings[source.id] = [mapping];
|
||||
}
|
||||
|
||||
log.info(`New measurement mapping added to source '${this._getSourceInfo(source)}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation for specific source.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {string} measurementId The measurement service measurement id
|
||||
* @return {Object} Source measurement schema
|
||||
*/
|
||||
getAnnotation(source, definition, measurementId) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mapping = this._getMappingByMeasurementSource(measurementId, definition);
|
||||
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
||||
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
const matchingMapping = this._getMatchingMapping(source, definition, measurement);
|
||||
|
||||
if (matchingMapping) {
|
||||
log.info('Matching mapping found:', matchingMapping);
|
||||
const { toSourceSchema, definition } = matchingMapping;
|
||||
return toSourceSchema(measurement, definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or update persisted measurements.
|
||||
*
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {Measurement} measurement The source measurement
|
||||
* @return {string} A measurement id
|
||||
*/
|
||||
addOrUpdate(source, definition, sourceMeasurement) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceInfo = this._getSourceInfo(source);
|
||||
|
||||
if (!definition) {
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._sourceHasMappings(source)) {
|
||||
log.warn(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let measurement = {};
|
||||
try {
|
||||
const sourceMappings = this.mappings[source.id];
|
||||
const { toMeasurementSchema } = sourceMappings.find(
|
||||
mapping => mapping.definition === definition
|
||||
);
|
||||
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(sourceMeasurement);
|
||||
|
||||
/* Assign measurement source instance */
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
log.error(`Failed to map '${sourceInfo}' measurement for definition ${definition}:`, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isValidMeasurement(measurement)) {
|
||||
log.warn(
|
||||
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let internalId = sourceMeasurement.id;
|
||||
if (!internalId) {
|
||||
internalId = guid();
|
||||
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||
}
|
||||
|
||||
const newMeasurement = {
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
id: internalId,
|
||||
};
|
||||
|
||||
if (this.measurements[internalId]) {
|
||||
log.info(`Measurement already defined. Updating measurement.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, source, newMeasurement);
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, source, newMeasurement);
|
||||
}
|
||||
|
||||
return newMeasurement.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to measurement updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {Function} callback Events callback
|
||||
* @return {Object} Observable object with actions
|
||||
*/
|
||||
subscribe(eventName, callback) {
|
||||
if (this._isValidEvent(eventName)) {
|
||||
const listenerId = guid();
|
||||
const subscription = { id: listenerId, callback };
|
||||
|
||||
console.info(`Subscribing to '${eventName}'.`);
|
||||
if (Array.isArray(this.listeners[eventName])) {
|
||||
this.listeners[eventName].push(subscription);
|
||||
} else {
|
||||
this.listeners[eventName] = [subscription];
|
||||
}
|
||||
|
||||
return {
|
||||
unsubscribe: () => this._unsubscribe(eventName, listenerId),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Event ${eventName} not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
_getMappingByMeasurementSource(measurementId, definition) {
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
if (this._isValidSource(measurement.source)) {
|
||||
return this.mappings[measurement.source.id].find(
|
||||
m => m.definition === definition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get measurement mapping function if matching criteria.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {string} measurement The measurement serice measurement
|
||||
* @return {Object} The mapping based on matched criteria
|
||||
*/
|
||||
_getMatchingMapping(source, definition, measurement) {
|
||||
const sourceMappings = this.mappings[source.id];
|
||||
|
||||
const sourceMappingsByDefinition = sourceMappings.filter(
|
||||
mapping => mapping.definition === definition
|
||||
);
|
||||
|
||||
/* Criteria Matching */
|
||||
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
|
||||
return (
|
||||
measurement.points &&
|
||||
measurement.points.length === matchingCriteria.points
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns formatted string with source info.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source
|
||||
* @return {string} Source information
|
||||
*/
|
||||
_getSourceInfo(source) {
|
||||
return `${source.name}@${source.version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given source is valid.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source
|
||||
* @return {boolean} Measurement source validation
|
||||
*/
|
||||
_isValidSource(source) {
|
||||
return source && this.sources[source.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given source has mappings.
|
||||
*
|
||||
* @param {MeasurementSource} source The measurement source
|
||||
* @return {boolean} Validation if source has mappings
|
||||
*/
|
||||
_sourceHasMappings(source) {
|
||||
return (
|
||||
Array.isArray(this.mappings[source.id]) && this.mappings[source.id].length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts measurement changes.
|
||||
*
|
||||
* @param {string} measurementId The measurement id
|
||||
* @param {MeasurementSource} source The measurement source
|
||||
* @param {string} eventName The event name
|
||||
* @return void
|
||||
*/
|
||||
_broadcastChange(eventName, source, measurement) {
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback({ source, measurement });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe to measurement updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {string} listenerId The listeners id
|
||||
* @return void
|
||||
*/
|
||||
_unsubscribe(eventName, listenerId) {
|
||||
if (!this.listeners[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = this.listeners[eventName];
|
||||
if (Array.isArray(listeners)) {
|
||||
this.listeners[eventName] = listeners.filter(
|
||||
({ id }) => id !== listenerId
|
||||
);
|
||||
} else {
|
||||
this.listeners[eventName] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given measurement data is valid.
|
||||
*
|
||||
* @param {Measurement} measurementData Measurement data
|
||||
* @return {boolean} Measurement validation
|
||||
*/
|
||||
_isValidMeasurement(measurementData) {
|
||||
Object.keys(measurementData).forEach(key => {
|
||||
if (!MEASUREMENT_SCHEMA_KEYS.includes(key)) {
|
||||
log.warn(`Invalid measurement key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given measurement service event is valid.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @return {boolean} Event name validation
|
||||
*/
|
||||
_isValidEvent(eventName) {
|
||||
return Object.values(this.EVENTS).includes(eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts object of objects to array.
|
||||
*
|
||||
* @return {Array} Array of objects
|
||||
*/
|
||||
_arrayOfObjects = obj => {
|
||||
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
|
||||
};
|
||||
}
|
||||
|
||||
export default MeasurementService;
|
||||
export { EVENTS, VALUE_TYPES };
|
||||
@@ -0,0 +1,405 @@
|
||||
import MeasurementService from './MeasurementService.js';
|
||||
import log from '../../log';
|
||||
|
||||
jest.mock('../../log.js', () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MeasurementService.js', () => {
|
||||
let measurementService;
|
||||
let measurement;
|
||||
let source;
|
||||
let definition;
|
||||
let matchingCriteria;
|
||||
let toAnnotation;
|
||||
let toMeasurement;
|
||||
let annotation;
|
||||
|
||||
beforeEach(() => {
|
||||
measurementService = new MeasurementService();
|
||||
source = measurementService.createSource('Test', '1');
|
||||
definition = 'Length';
|
||||
annotation = {
|
||||
toolName: definition,
|
||||
measurementData: {},
|
||||
};
|
||||
measurement = {
|
||||
sopInstanceUID: '123',
|
||||
frameOfReferenceUID: '1234',
|
||||
referenceSeriesUID: '12345',
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
unit: 'mm',
|
||||
area: 123,
|
||||
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
|
||||
source: source,
|
||||
};
|
||||
toAnnotation = () => annotation;
|
||||
toMeasurement = () => measurement;
|
||||
matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
};
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createSource()', () => {
|
||||
it('creates new source with name and version', () => {
|
||||
measurementService.createSource('Testing', '1');
|
||||
});
|
||||
|
||||
it('logs warning and return early if no name provided', () => {
|
||||
measurementService.createSource(null, '1');
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no version provided', () => {
|
||||
measurementService.createSource('Testing', null);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMapping()', () => {
|
||||
it('adds new mapping', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no matching criteria provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
null,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if invalid source provided', () => {
|
||||
const invalidSoure = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
invalidSoure,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no source provided', () => {
|
||||
measurementService.addMapping(
|
||||
null /* source */,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no definition provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
null /* definition */,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no measurement mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
null /* toAnnotation */,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no annotation mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
null /* toMeasurement */
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnnotation()', () => {
|
||||
it('get annotation based on matched criteria', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
const mappedAnnotation = source.getAnnotation(definition, measurementId);
|
||||
|
||||
expect(annotation).toBe(mappedAnnotation);
|
||||
});
|
||||
|
||||
it('get annotation based on source and definition', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
{},
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
const mappedAnnotation = source.getAnnotation(definition, measurementId);
|
||||
|
||||
expect(annotation).toBe(mappedAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeasurements()', () => {
|
||||
it('return all measurement service measurements', () => {
|
||||
const anotherMeasurement = {
|
||||
...measurement,
|
||||
label: 'Label2',
|
||||
unit: 'HU',
|
||||
};
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
source.addOrUpdate(definition, anotherMeasurement);
|
||||
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
expect(measurements.length).toEqual(2);
|
||||
expect(measurements.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeasurement()', () => {
|
||||
it('return measurement service measurement with given id', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
const returnedMeasurement = measurementService.getMeasurement(id);
|
||||
|
||||
/* Clear dynamic data */
|
||||
delete returnedMeasurement.modifiedTimestamp;
|
||||
|
||||
expect({ id, ...measurement }).toEqual(returnedMeasurement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addOrUpdate()', () => {
|
||||
it('adds new measurements', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
expect(measurements.length).toBe(2);
|
||||
});
|
||||
|
||||
it('fails to add new measurements when no mapping', () => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('fails to add new measurements when invalid mapping function', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
1 /* Invalid */
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.error.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('adds new measurement with custom id', () => {
|
||||
const newMeasurement = { id: 1, ...measurement };
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
/* Add new measurement */
|
||||
source.addOrUpdate(definition, newMeasurement);
|
||||
const savedMeasurement = measurementService.getMeasurement(newMeasurement.id);
|
||||
|
||||
/* Clear dynamic data */
|
||||
delete newMeasurement.modifiedTimestamp;
|
||||
delete savedMeasurement.modifiedTimestamp;
|
||||
|
||||
expect(newMeasurement).toEqual(savedMeasurement);
|
||||
});
|
||||
|
||||
it('logs warning and return if adding invalid measurement', () => {
|
||||
measurement.invalidProperty = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('updates existent measurement', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
|
||||
measurement.unit = 'HU';
|
||||
|
||||
source.addOrUpdate(definition, { id, ...measurement });
|
||||
const updatedMeasurement = measurementService.getMeasurement(id);
|
||||
|
||||
expect(updatedMeasurement.unit).toBe('HU');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe()', () => {
|
||||
it('subscribers receive broadcasted add event', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const { MEASUREMENT_ADDED } = measurementService.EVENTS;
|
||||
let addCallbackWasCalled = false;
|
||||
|
||||
/* Subscribe to add event */
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
() => (addCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Add new measurement */
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(addCallbackWasCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('subscribers receive broadcasted update event', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const { MEASUREMENT_UPDATED } = measurementService.EVENTS;
|
||||
let updateCallbackWasCalled = false;
|
||||
|
||||
/* Subscribe to update event */
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_UPDATED,
|
||||
() => (updateCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Create measurement */
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
|
||||
/* Update measurement */
|
||||
source.addOrUpdate(definition, { id, ...measurement });
|
||||
|
||||
expect(updateCallbackWasCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribes a listener', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
let updateCallbackWasCalled = false;
|
||||
const { MEASUREMENT_ADDED } = measurementService.EVENTS;
|
||||
|
||||
/* Subscribe to Add event */
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
() => (updateCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Unsubscribe */
|
||||
unsubscribe();
|
||||
|
||||
/* Create measurement */
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(updateCallbackWasCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import MeasurementService from './MeasurementService';
|
||||
|
||||
export default {
|
||||
name: 'MeasurementService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new MeasurementService();
|
||||
},
|
||||
};
|
||||
@@ -2,10 +2,12 @@ import ServicesManager from './ServicesManager.js';
|
||||
import UINotificationService from './UINotificationService';
|
||||
import UIModalService from './UIModalService';
|
||||
import UIDialogService from './UIDialogService';
|
||||
import MeasurementService from './MeasurementService';
|
||||
|
||||
export {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
ServicesManager,
|
||||
MeasurementService,
|
||||
};
|
||||
Reference in new issue
Block a user