feat(OHIF):Add DICOM Video Display
* feat(OHIFv3):Add a video viewport * Fixed a couple of viewing issues * fix(OHIF):If not all instances are used by a given provider, then continue to add items * Added video data * Add a basic e2e test for video display * Adding a screen shot as requested
This commit is contained in:
parent
73aa738c48
commit
ca9ed62a5f
@ -186,7 +186,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
}
|
||||
|
||||
const viewportInfo = getEnabledElement(i);
|
||||
const hasCornerstoneContext = viewportInfo.context &&
|
||||
if (!viewportInfo) continue;
|
||||
const hasCornerstoneContext =
|
||||
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
||||
|
||||
if (hasCornerstoneContext) {
|
||||
|
||||
@ -149,6 +149,33 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
},
|
||||
},
|
||||
retrieve: {
|
||||
/* Generates a URL that can be used for direct retrieve of the bulkdata */
|
||||
directURL: (params) => {
|
||||
const { instance, tag = "PixelData", defaultPath = "/pixeldata", defaultType = "video/mp4" } = params;
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
// If the BulkDataURI isn't present, then assume it uses the pixeldata endpoint
|
||||
// The standard isn't quite clear on that, but appears to be what is expected
|
||||
const value = instance[tag];
|
||||
const BulkDataURI =
|
||||
(value && value.BulkDataURI) ||
|
||||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
||||
const hasQuery = BulkDataURI.indexOf('?') != -1;
|
||||
const hasAccept = BulkDataURI.indexOf('accept=') != -1;
|
||||
const acceptUri =
|
||||
BulkDataURI +
|
||||
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
|
||||
if (BulkDataURI.indexOf('http') == 0) return acceptUri;
|
||||
if (BulkDataURI.indexOf('/') == 0) {
|
||||
return wadoRoot + acceptUri;
|
||||
}
|
||||
if (BulkDataURI.indexOf('series/') == 0) {
|
||||
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
||||
}
|
||||
if (BulkDataURI.indexOf('instances/') === 0) {
|
||||
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
|
||||
}
|
||||
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
|
||||
},
|
||||
series: {
|
||||
metadata: async ({
|
||||
StudyInstanceUID,
|
||||
|
||||
8
extensions/dicom-video/.webpack/webpack.dev.js
Normal file
8
extensions/dicom-video/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
44
extensions/dicom-video/.webpack/webpack.prod.js
Normal file
44
extensions/dicom-video/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,44 @@
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtDICOMSR',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
21
extensions/dicom-video/LICENSE
Normal file
21
extensions/dicom-video/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
15
extensions/dicom-video/README.md
Normal file
15
extensions/dicom-video/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# DICOM Video
|
||||
This extension adds support for displaying DICOM video objects in a script tag.
|
||||
The video data must currently be available as video/mp4 on the BulkDataURI that
|
||||
is provided in the DICOMweb metadata response, and the video must have one of the
|
||||
specified SOP Class UID's in order to be recognized by the SOP class handler.
|
||||
|
||||
Those are:
|
||||
* Video Microscop Image Storage
|
||||
* Video Photographic Image Storage
|
||||
* Video Endoscopic Image Storage
|
||||
* Secondary Capture Image Storage
|
||||
* Multiframe True Color Secondary Capture Image Storage
|
||||
|
||||
The extension is a "standard" extension in that it is installed and available
|
||||
by default.
|
||||
1
extensions/dicom-video/babel.config.js
Normal file
1
extensions/dicom-video/babel.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
49
extensions/dicom-video/package.json
Normal file
49
extensions/dicom-video/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-video",
|
||||
"version": "0.0.1",
|
||||
"description": "OHIF extension for video display",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.6.0",
|
||||
"cornerstone-math": "^0.1.9",
|
||||
"cornerstone-tools": "6.0.2",
|
||||
"cornerstone-wado-image-loader": "4.0.4",
|
||||
"dcmjs": "0.16.1",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-cornerstone-viewport": "4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
"classnames": "^2.2.6"
|
||||
}
|
||||
}
|
||||
84
extensions/dicom-video/src/getSopClassHandlerModule.js
Normal file
84
extensions/dicom-video/src/getSopClassHandlerModule.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||
import { utils, classes } from '@ohif/core';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
|
||||
VIDEO_PHOTOGRAPHIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.4.1',
|
||||
VIDEO_ENDOSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.1.1',
|
||||
/** Need to use fallback, could be video or image */
|
||||
SECONDARY_CAPTURE_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.7',
|
||||
MULTIFRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE:
|
||||
'1.2.840.10008.5.1.4.1.1.7.4',
|
||||
};
|
||||
|
||||
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
const SupportedTransferSyntaxes = {
|
||||
MPEG4_AVC_264_HIGH_PROFILE: '1.2.840.10008.1.2.4.102',
|
||||
MPEG4_AVC_264_BD_COMPATIBLE_HIGH_PROFILE: '1.2.840.10008.1.2.4.103',
|
||||
MPEG4_AVC_264_HIGH_PROFILE_FOR_2D_VIDEO: '1.2.840.10008.1.2.4.104',
|
||||
MPEG4_AVC_264_HIGH_PROFILE_FOR_3D_VIDEO: '1.2.840.10008.1.2.4.105',
|
||||
MPEG4_AVC_264_STEREO_HIGH_PROFILE: '1.2.840.10008.1.2.4.106',
|
||||
HEVC_265_MAIN_PROFILE: '1.2.840.10008.1.2.4.107',
|
||||
HEVC_265_MAIN_10_PROFILE: '1.2.840.10008.1.2.4.108',
|
||||
};
|
||||
|
||||
const supportedTransferSyntaxUIDs = Object.values(SupportedTransferSyntaxes);
|
||||
|
||||
const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => {
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
return instances
|
||||
.filter(metadata => {
|
||||
const tsuid = metadata.AvailableTransferSyntaxUID ||
|
||||
metadata.TransferSyntaxUID || metadata['00083002'];
|
||||
return supportedTransferSyntaxUIDs.includes(tsuid);
|
||||
})
|
||||
.map(instance => {
|
||||
const { Modality, FrameOfReferenceUID, SOPInstanceUID } = instance;
|
||||
const { SeriesDescription, ContentDate, ContentTime } = instance;
|
||||
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance;
|
||||
const displaySet = {
|
||||
//plugin: id,
|
||||
Modality,
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
videoUrl: dataSource.retrieve.directURL({ instance }),
|
||||
others: [instance],
|
||||
thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }),
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
sopClassUids,
|
||||
numImageFrames: NumberOfFrames,
|
||||
instance,
|
||||
};
|
||||
return displaySet;
|
||||
});
|
||||
};
|
||||
|
||||
export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: SOPClassHandlerName,
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
];
|
||||
}
|
||||
7
extensions/dicom-video/src/id.js
Normal file
7
extensions/dicom-video/src/id.js
Normal file
@ -0,0 +1,7 @@
|
||||
const id = 'org.ohif.dicom-video';
|
||||
|
||||
export default id;
|
||||
|
||||
const SOPClassHandlerName = 'dicom-video';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
export { SOPClassHandlerName, SOPClassHandlerId };
|
||||
99
extensions/dicom-video/src/index.js
Normal file
99
extensions/dicom-video/src/index.js
Normal file
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import id from './id.js';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneVideoViewport'
|
||||
);
|
||||
});
|
||||
|
||||
const OHIFCornerstoneVideoViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
dependencies: [
|
||||
// TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future.
|
||||
// OHIF-229
|
||||
{
|
||||
id: 'org.ohif.cornerstone',
|
||||
version: '3.0.0',
|
||||
},
|
||||
{
|
||||
id: 'org.ohif.measurement-tracking',
|
||||
version: '^0.0.1',
|
||||
},
|
||||
],
|
||||
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
// No-op for now
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneVideoViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneVideoViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }];
|
||||
},
|
||||
getCommandsModule({ servicesManager }) {
|
||||
return {
|
||||
definitions: {
|
||||
setToolActive: {
|
||||
commandFn: ({ toolName, element }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
}
|
||||
|
||||
// Set same tool or alt tool
|
||||
const toolAlias = _getToolAlias(toolName);
|
||||
|
||||
cornerstoneTools.setToolActiveForElement(element, toolAlias, {
|
||||
mouseButtonMask: 1,
|
||||
});
|
||||
},
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VIDEO',
|
||||
};
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
function _getToolAlias(toolName) {
|
||||
let toolAlias = toolName;
|
||||
|
||||
switch (toolName) {
|
||||
case 'EllipticalRoi':
|
||||
toolAlias = 'SREllipticalRoi';
|
||||
break;
|
||||
}
|
||||
|
||||
return toolAlias;
|
||||
}
|
||||
61
extensions/dicom-video/src/tools/modules/dicomVideoModule.js
Normal file
61
extensions/dicom-video/src/tools/modules/dicomVideoModule.js
Normal file
@ -0,0 +1,61 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const state = {
|
||||
TrackingUniqueIdentifier: null,
|
||||
trackingIdentifiersByEnabledElementUUID: {},
|
||||
};
|
||||
|
||||
function setTrackingUniqueIdentifiersForElement(
|
||||
element,
|
||||
trackingUniqueIdentifiers,
|
||||
activeIndex = 0
|
||||
) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
state.trackingIdentifiersByEnabledElementUUID[uuid] = {
|
||||
trackingUniqueIdentifiers,
|
||||
activeIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function setActiveTrackingUniqueIdentifierForElement(
|
||||
element,
|
||||
TrackingUniqueIdentifier
|
||||
) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
const trackingIdentifiersForElement =
|
||||
state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||
|
||||
if (trackingIdentifiersForElement) {
|
||||
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
|
||||
tuid => tuid === TrackingUniqueIdentifier
|
||||
);
|
||||
|
||||
trackingIdentifiersForElement.activeIndex = activeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrackingUniqueIdentifiersForElement(element) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
if (state.trackingIdentifiersByEnabledElementUUID[uuid]) {
|
||||
return state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||
}
|
||||
|
||||
return { trackingUniqueIdentifiers: [] };
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters: {
|
||||
trackingUniqueIdentifiersForElement: getTrackingUniqueIdentifiersForElement,
|
||||
},
|
||||
setters: {
|
||||
trackingUniqueIdentifiersForElement: setTrackingUniqueIdentifiersForElement,
|
||||
activeTrackingUniqueIdentifierForElement: setActiveTrackingUniqueIdentifierForElement,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
} from '@ohif/ui';
|
||||
import { adapters } from 'dcmjs';
|
||||
import id from '../id';
|
||||
|
||||
|
||||
function OHIFCornerstoneVideoViewport({
|
||||
children,
|
||||
dataSource,
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
const {
|
||||
DisplaySetService,
|
||||
MeasurementService,
|
||||
ToolBarService,
|
||||
} = servicesManager.services;
|
||||
const { videoUrl } = displaySet;
|
||||
const mimeType = "video/mp4";
|
||||
|
||||
// Need to copies of the source to fix a firefox bug
|
||||
return (
|
||||
<div className="bg-primary-black w-full h-full">
|
||||
<video
|
||||
src={videoUrl}
|
||||
controls
|
||||
controlsList="nodownload"
|
||||
preload="auto"
|
||||
className="w-full h-full"
|
||||
>
|
||||
<source src={videoUrl} type={mimeType} />
|
||||
<source src={videoUrl} type={mimeType} />
|
||||
Video src/type not supported: <a href={videoUrl}>{videoUrl} of type {mimeType}</a>
|
||||
</video>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneVideoViewport;
|
||||
BIN
extensions/dicom-video/video-screenshot.jpg
Normal file
BIN
extensions/dicom-video/video-screenshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
@ -18,6 +18,11 @@ const dicomsr = {
|
||||
viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr',
|
||||
};
|
||||
|
||||
const dicomvideo = {
|
||||
sopClassHandler: 'org.ohif.dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: 'org.ohif.dicom-video.viewportModule.dicom-video',
|
||||
}
|
||||
|
||||
export default function mode({ modeConfiguration }) {
|
||||
return {
|
||||
// TODO: We're using this as a route segment
|
||||
@ -87,6 +92,10 @@ export default function mode({ modeConfiguration }) {
|
||||
namespace: dicomsr.viewport,
|
||||
displaySetsToDisplay: [dicomsr.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -98,9 +107,10 @@ export default function mode({ modeConfiguration }) {
|
||||
'org.ohif.cornerstone',
|
||||
'org.ohif.measurement-tracking',
|
||||
'org.ohif.dicom-sr',
|
||||
'org.ohif.dicom-video',
|
||||
],
|
||||
hangingProtocols: [ohif.hangingProtocols],
|
||||
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
|
||||
sopClassHandlers: [dicomvideo.sopClassHandler, ohif.sopClassHandler, dicomsr.sopClassHandler,],
|
||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,6 +3,31 @@ import EVENTS from './EVENTS';
|
||||
|
||||
const displaySetCache = [];
|
||||
|
||||
/**
|
||||
* Find an instance in a list of instances, comparing by SOP instance UID
|
||||
*/
|
||||
const findInSet = (instance, list) => {
|
||||
if (!list) return false;
|
||||
for (const elem of list) {
|
||||
if (!elem) continue;
|
||||
if (elem === instance) return true;
|
||||
if (elem.SOPInstanceUID === instance.SOPInstanceUID) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find an instance in a display set
|
||||
* @returns true if found
|
||||
*/
|
||||
const findInstance = (instance, displaySets) => {
|
||||
for (const displayset of displaySets) {
|
||||
if (findInSet(instance, displayset.images)) return true;
|
||||
if (findInSet(instance, displayset.others)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class DisplaySetService {
|
||||
constructor() {
|
||||
this.activeDisplaySets = [];
|
||||
@ -151,13 +176,15 @@ export default class DisplaySetService {
|
||||
}
|
||||
};
|
||||
|
||||
makeDisplaySetForInstances(instances, settings) {
|
||||
makeDisplaySetForInstances(instancesSrc, settings) {
|
||||
let instances = instancesSrc;
|
||||
const instance = instances[0];
|
||||
|
||||
const existingDisplaySets =
|
||||
this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || [];
|
||||
|
||||
const SOPClassHandlerIds = this.SOPClassHandlerIds;
|
||||
let allDisplaySets;
|
||||
|
||||
for (let i = 0; i < SOPClassHandlerIds.length; i++) {
|
||||
const SOPClassHandlerId = SOPClassHandlerIds[i];
|
||||
@ -174,6 +201,8 @@ export default class DisplaySetService {
|
||||
} else {
|
||||
displaySets = handler.getDisplaySetsFromSeries(instances);
|
||||
|
||||
if (!displaySets || !displaySets.length) continue;
|
||||
|
||||
// applying hp-defined viewport settings to the displaysets
|
||||
displaySets.forEach(ds => {
|
||||
Object.keys(settings).forEach(key => {
|
||||
@ -183,10 +212,15 @@ export default class DisplaySetService {
|
||||
|
||||
this._addDisplaySetsToCache(displaySets);
|
||||
this._addActiveDisplaySets(displaySets);
|
||||
|
||||
instances = instances.filter(instance => !findInstance(instance, displaySets))
|
||||
}
|
||||
|
||||
return displaySets;
|
||||
allDisplaySets = allDisplaySets ? [...allDisplaySets, ...displaySets] : displaySets;
|
||||
|
||||
if (!instances.length) return allDisplaySets;
|
||||
}
|
||||
}
|
||||
return allDisplaySets;
|
||||
}
|
||||
}
|
||||
|
||||
24
platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js
Normal file
24
platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js
Normal file
@ -0,0 +1,24 @@
|
||||
describe('OHIF Video Display', function () {
|
||||
before(() => {
|
||||
cy.openStudyInViewer(
|
||||
'2.25.96975534054447904995905761963464388233'
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
cy.resetViewport().wait(50);
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', function () {
|
||||
cy.get('[data-cy="study-browser-thumbnail"]')
|
||||
.its('length')
|
||||
.should('be.gt', 1);
|
||||
});
|
||||
|
||||
it('performs double-click to load thumbnail in active viewport', () => {
|
||||
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
|
||||
|
||||
//const expectedText = 'Ser: 3';
|
||||
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);
|
||||
});
|
||||
});
|
||||
@ -23,6 +23,7 @@ import OHIFDefaultExtension from '@ohif/extension-default';
|
||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
|
||||
import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr';
|
||||
import OHIFDICOMVIDEOExtension from '@ohif/extension-dicom-video';
|
||||
|
||||
/** Combine our appConfiguration and "baked-in" extensions */
|
||||
const appProps = {
|
||||
@ -32,6 +33,7 @@ const appProps = {
|
||||
OHIFCornerstoneExtension,
|
||||
OHIFMeasurementTrackingExtension,
|
||||
OHIFDICOMSRExtension,
|
||||
OHIFDICOMVIDEOExtension,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
2
testdata
2
testdata
@ -1 +1 @@
|
||||
Subproject commit 47d51e15a0d41c02f806570d8cf78ab39e782387
|
||||
Subproject commit e8e18cb65ff77e37273a8553f0f651ba4fd1a731
|
||||
Loading…
Reference in New Issue
Block a user