diff --git a/extensions/default/src/ActionButtons.jsx b/extensions/default/src/ActionButtons.jsx
deleted file mode 100644
index 0e3e8a3b6..000000000
--- a/extensions/default/src/ActionButtons.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
-
-function ActionButtons() {
- return (
-
- alert('Export')}>
-
-
-
-
-
-
-
- );
-}
-
-export default ActionButtons;
diff --git a/extensions/default/src/PanelMeasurementTable.js b/extensions/default/src/PanelMeasurementTable.js
index 6da4696fd..0967ac378 100644
--- a/extensions/default/src/PanelMeasurementTable.js
+++ b/extensions/default/src/PanelMeasurementTable.js
@@ -1,55 +1,170 @@
-import React from 'react';
-import { StudySummary, MeasurementTable } from '@ohif/ui';
-import ActionButtons from './ActionButtons.jsx';
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { MeasurementTable } from '@ohif/ui';
+import { DicomMetadataStore } from '@ohif/core';
+import debounce from './debounce.js';
export default function PanelMeasurementTable({
servicesManager,
- commandsManager,
+ // commandsManager,
}) {
const { MeasurementService } = servicesManager.services;
+ const [displayMeasurements, setDisplayMeasurements] = useState([]);
- console.log('MeasurementTable rendering!!!!!!!!!!!!!');
+ useEffect(() => {
+ const debouncedSetDisplayMeasurements = debounce(
+ setDisplayMeasurements,
+ 100
+ );
+ // ~~ Initial
+ setDisplayMeasurements(_getMappedMeasurements(MeasurementService));
- const descriptionData = {
- date: '07-Sep-2010',
- modality: 'CT',
- description: 'CHEST/ABD/PELVIS W CONTRAST',
- };
+ // ~~ Subscription
+ const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
+ const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
+ const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
+ const subscriptions = [];
- const activeMeasurementItem = 0;
+ [added, updated, removed].forEach(evt => {
+ subscriptions.push(
+ MeasurementService.subscribe(evt, () => {
+ debouncedSetDisplayMeasurements(
+ _getMappedMeasurements(MeasurementService)
+ );
+ }).unsubscribe
+ );
+ });
- const measurementTableData = {
- title: 'Measurements',
- amount: 10,
- data: new Array(10).fill({}).map((el, i) => ({
- id: i + 1,
- label: 'Label short description',
- displayText: '24.0 x 24.0 mm (S:4, I:22)',
- isActive: activeMeasurementItem === i + 1,
- })),
- onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
- onEdit: id => alert(`Edit: ${id}`),
- };
+ return () => {
+ subscriptions.forEach(unsub => {
+ unsub();
+ });
+ };
+ }, [MeasurementService]);
+
+ // const activeMeasurementItem = 0;
return (
<>
-
{}}
onEdit={id => alert(`Edit: ${id}`)}
/>
-
>
);
}
+
+PanelMeasurementTable.propTypes = {
+ servicesManager: PropTypes.shape({
+ services: PropTypes.shape({
+ MeasurementService: PropTypes.shape({
+ getMeasurements: PropTypes.func.isRequired,
+ subscribe: PropTypes.func.isRequired,
+ EVENTS: PropTypes.object.isRequired,
+ VALUE_TYPES: PropTypes.object.isRequired,
+ }).isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+function _getMappedMeasurements(MeasurementService) {
+ const measurements = MeasurementService.getMeasurements();
+ const mappedMeasurements = measurements.map((m, index) =>
+ _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
+ );
+
+ return mappedMeasurements;
+}
+
+function _mapMeasurementToDisplay(measurement, index, types) {
+ const {
+ id,
+ label,
+ description,
+ // Reference IDs
+ referenceStudyUID,
+ referenceSeriesUID,
+ SOPInstanceUID,
+ } = measurement;
+ const instance = DicomMetadataStore.getInstance(
+ referenceStudyUID,
+ referenceSeriesUID,
+ SOPInstanceUID
+ );
+ const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
+
+ return {
+ id: index + 1,
+ label: '(empty)', // 'Label short description',
+ displayText:
+ _getDisplayText(
+ measurement,
+ PixelSpacing,
+ SeriesNumber,
+ InstanceNumber,
+ types
+ ) || [],
+ // TODO: handle one layer down
+ isActive: false, // activeMeasurementItem === i + 1,
+ };
+}
+
+function _getDisplayText(
+ measurement,
+ pixelSpacing,
+ seriesNumber,
+ instanceNumber,
+ types
+) {
+ const { type, points } = measurement;
+ const hasPixelSpacing =
+ pixelSpacing !== undefined &&
+ Array.isArray(pixelSpacing) &&
+ pixelSpacing.length === 2;
+ const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing
+ ? pixelSpacing
+ : [1, 1];
+ const unit = hasPixelSpacing ? 'mm' : 'px';
+
+ switch (type) {
+ case types.POLYLINE: {
+ const { length } = measurement;
+ const roundedLength = _round(length, 1);
+
+ return [
+ `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
+ ];
+ }
+ case types.BIDIRECTIONAL: {
+ const { shortestDiameter, longestDiameter } = measurement;
+ const roundedShortestDiameter = _round(shortestDiameter, 1);
+ const roundedLongestDiameter = _round(longestDiameter, 1);
+
+ return [
+ `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
+ `s: ${roundedShortestDiameter} ${unit}`,
+ ];
+ }
+ case types.ELLIPSE: {
+ const { area } = measurement;
+ const roundedArea = _round(area, 1);
+
+ return [
+ `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
+ ];
+ }
+ case types.POINT: {
+ const { text } = measurement;
+ return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
+ }
+ }
+}
+
+function _round(value, decimals) {
+ return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
+}
diff --git a/extensions/default/src/debounce.js b/extensions/default/src/debounce.js
new file mode 100644
index 000000000..83a16b099
--- /dev/null
+++ b/extensions/default/src/debounce.js
@@ -0,0 +1,21 @@
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+function debounce(func, wait, immediate) {
+ var timeout;
+ return function() {
+ var context = this,
+ args = arguments;
+ var later = function() {
+ timeout = null;
+ if (!immediate) func.apply(context, args);
+ };
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(context, args);
+ };
+}
+
+export default debounce;
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
index 9a40f8ca5..e8381f39f 100644
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
@@ -21,7 +21,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
measurementChangeTimestamp,
200
);
- const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services;
+ const {
+ MeasurementService,
+ UINotificationService,
+ UIDialogService,
+ DisplaySetService,
+ } = servicesManager.services;
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
@@ -31,9 +36,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
);
const [displayMeasurements, setDisplayMeasurements] = useState([]);
- // TODO: measurements subscribtion
- // Initial?
useEffect(() => {
const measurements = MeasurementService.getMeasurements();
const filteredMeasurements = measurements.filter(
@@ -120,7 +123,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
isDraggable: false,
centralize: true,
// TODO: Create a loading indicator component + zeplin design?
- content: () => Loading...
+ content: () => Loading...
,
});
try {
@@ -136,13 +139,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
// Would need some way of saying which one is the "push" dataSource
const dataSource = dataSources[0];
- const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
+ const naturalizedReport = await DICOMSR.storeMeasurements(
+ trackedMeasurements,
+ dataSource
+ );
- DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true });
+ DisplaySetService.makeDisplaySets([naturalizedReport], {
+ madeInClient: true,
+ });
UINotificationService.show({
title: 'STOW SR',
message: 'Measurements saved successfully',
- type: 'success'
+ type: 'success',
});
} catch (error) {
UINotificationService.show({
@@ -169,7 +177,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
title="Measurements"
amount={displayMeasurements.length}
data={displayMeasurements}
- onClick={() => { }}
+ onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
@@ -183,7 +191,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
);
}
-PanelMeasurementTableTracking.propTypes = {};
+PanelMeasurementTableTracking.propTypes = {
+ servicesManager: PropTypes.shape({
+ services: PropTypes.shape({
+ MeasurementService: PropTypes.shape({
+ getMeasurements: PropTypes.func.isRequired,
+ VALUE_TYPES: PropTypes.object.isRequired,
+ }).isRequired,
+ }).isRequired,
+ }).isRequired,
+};
// TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, index, types) {
@@ -203,9 +220,6 @@ function _mapMeasurementToDisplay(measurement, index, types) {
);
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
- console.log('mapping....', measurement);
- console.log(instance);
-
return {
id: index + 1,
label: '(empty)', // 'Label short description',
@@ -234,15 +248,7 @@ function _getDisplayText(
instanceNumber,
types
) {
- // TODO: determination of shape influences text
- // Length: 'xx.x unit (S:x, I:x)'
- // Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
- // Ellipse?
- // Bidirectional?
- // Freehand?
-
const { type, points } = measurement;
-
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
@@ -253,18 +259,16 @@ function _getDisplayText(
const unit = hasPixelSpacing ? 'mm' : 'px';
switch (type) {
- case types.POLYLINE:
+ case types.POLYLINE: {
const { length } = measurement;
-
const roundedLength = _round(length, 1);
return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
];
-
- case types.BIDIRECTIONAL:
+ }
+ case types.BIDIRECTIONAL: {
const { shortestDiameter, longestDiameter } = measurement;
-
const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1);
@@ -272,16 +276,19 @@ function _getDisplayText(
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`s: ${roundedShortestDiameter} ${unit}`,
];
- case types.ELLIPSE:
+ }
+ case types.ELLIPSE: {
const { area } = measurement;
-
const roundedArea = _round(area, 1);
+
return [
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
];
- case types.POINT:
+ }
+ case types.POINT: {
const { text } = measurement;
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
+ }
}
}
diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js
index 2bf100c09..db09204ba 100644
--- a/modes/longitudinal/src/index.js
+++ b/modes/longitudinal/src/index.js
@@ -20,8 +20,8 @@ export default function mode({ modeConfiguration }) {
return {
// TODO: We're using this as a route segment
// We should not be.
- id: 'longitudinal-workflow',
- displayName: 'Comparison',
+ id: 'viewer',
+ displayName: 'Basic Viewer',
validationTags: {
study: [],
series: [],
@@ -95,9 +95,7 @@ export default function mode({ modeConfiguration }) {
'org.ohif.dicom-sr',
],
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
- hotkeys: [
- ...hotkeys.defaults.hotkeyBindings
- ]
+ hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
diff --git a/modes/example/.webpack/webpack.dev.js b/modes/segmentation/.webpack/webpack.dev.js
similarity index 100%
rename from modes/example/.webpack/webpack.dev.js
rename to modes/segmentation/.webpack/webpack.dev.js
diff --git a/modes/example/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js
similarity index 100%
rename from modes/example/.webpack/webpack.prod.js
rename to modes/segmentation/.webpack/webpack.prod.js
diff --git a/modes/example/LICENSE b/modes/segmentation/LICENSE
similarity index 100%
rename from modes/example/LICENSE
rename to modes/segmentation/LICENSE
diff --git a/modes/example/babel.config.js b/modes/segmentation/babel.config.js
similarity index 100%
rename from modes/example/babel.config.js
rename to modes/segmentation/babel.config.js
diff --git a/modes/example/package.json b/modes/segmentation/package.json
similarity index 90%
rename from modes/example/package.json
rename to modes/segmentation/package.json
index e98a3601e..66ba594e8 100644
--- a/modes/example/package.json
+++ b/modes/segmentation/package.json
@@ -1,7 +1,7 @@
{
- "name": "@ohif/mode-example",
+ "name": "@ohif/mode-segmentation",
"version": "0.0.1",
- "description": "Example mode for OHIF",
+ "description": "Segmentation mode for OHIF",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
diff --git a/modes/example/src/index.js b/modes/segmentation/src/index.js
similarity index 88%
rename from modes/example/src/index.js
rename to modes/segmentation/src/index.js
index 597f179ca..fc56ce4ed 100644
--- a/modes/example/src/index.js
+++ b/modes/segmentation/src/index.js
@@ -3,8 +3,10 @@ import { hotkeys } from '@ohif/core';
export default function mode({ modeConfiguration }) {
return {
- id: 'example-mode',
- displayName: 'Basic Viewer',
+ // TODO: Mode uses 'id' for route when it should use `slug`, if provided, and
+ // the route path
+ id: 'segmentation',
+ displayName: 'Segmentation',
validationTags: {
study: [],
series: [],
@@ -15,7 +17,7 @@ export default function mode({ modeConfiguration }) {
},
routes: [
{
- path: 'viewer',
+ path: 'segmentation',
init: ({ servicesManager, extensionManager }) => {
const { ToolBarService } = servicesManager.services;
ToolBarService.init(extensionManager);
@@ -51,7 +53,6 @@ export default function mode({ modeConfiguration }) {
return {
id: 'org.ohif.default.layoutTemplateModule.viewerLayout',
props: {
- // named slots
leftPanels: ['org.ohif.default.panelModule.seriesList'],
rightPanels: ['org.ohif.default.panelModule.measure'],
viewports: [
@@ -69,10 +70,8 @@ export default function mode({ modeConfiguration }) {
],
extensions: ['org.ohif.default', 'org.ohif.cornerstone'],
sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'],
- hotkeys: [
- ...hotkeys.defaults.hotkeyBindings
- ]
+ hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
-window.exampleMode = mode({});
+window.segmentationMode = mode({});
diff --git a/modes/example/src/toolbarButtons.js b/modes/segmentation/src/toolbarButtons.js
similarity index 100%
rename from modes/example/src/toolbarButtons.js
rename to modes/segmentation/src/toolbarButtons.js
diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx
index 1cb815d60..1e72bd772 100644
--- a/platform/viewer/src/App.jsx
+++ b/platform/viewer/src/App.jsx
@@ -20,8 +20,8 @@ import createRoutes from './routes';
import appInit from './appInit.js';
// TODO: Temporarily for testing
-import '@ohif/mode-example';
import '@ohif/mode-longitudinal';
+import '@ohif/mode-segmentation';
/**
* ENV Variable to determine routing behavior
@@ -50,7 +50,7 @@ function App({ config, defaultExtensions }) {
dataSources,
extensionManager,
servicesManager,
- hotkeysManager
+ hotkeysManager,
});
const {
UIDialogService,
diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js
index eb14613ce..3c35dd487 100644
--- a/platform/viewer/src/appInit.js
+++ b/platform/viewer/src/appInit.js
@@ -71,8 +71,8 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// TODO: Remove this
if (!appConfig.modes.length) {
- appConfig.modes.push(window.exampleMode);
appConfig.modes.push(window.longitudinalMode);
+ appConfig.modes.push(window.segmentationMode);
}
return {
@@ -80,7 +80,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
commandsManager,
extensionManager,
servicesManager,
- hotkeysManager
+ hotkeysManager,
};
}