diff --git a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html deleted file mode 100644 index 2eb628817..000000000 --- a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - {{> studyBrowser (clone this)}} - - - {{> React component=ViewerMain studies=this.studies}} - - - {{> measurementLightTable (clone this)}} - - - diff --git a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.js b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.js index b66aa7a45..4967c45e3 100644 --- a/OHIFViewer/client/components/flexboxLayout/flexboxLayout.js +++ b/OHIFViewer/client/components/flexboxLayout/flexboxLayout.js @@ -1,15 +1,176 @@ +import { Component } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; + import { Viewerbase } from 'meteor/ohif:viewerbase'; +import { StudyBrowser } from 'react-viewerbase'; -Template.flexboxLayout.helpers({ - leftSidebarOpen() { - return Template.instance().data.state.get('leftSidebar'); - }, +/** + * Asynchronous wrapper around Cornerstone's renderToCanvas method. + * + * @param {HTMLElement} canvasElement An HTML element + * @param {Image} image A Cornerstone Image + * + * @return {Promise} A promise tracking the progress of the rendering. Resolves empty. + */ +function renderAsync(canvasElement, image) { + return new Promise((resolve, reject) => { + try { + cornerstone.renderToCanvas(canvasElement, image); + resolve(); + } catch(error) { + reject(error); + } + }); +} - rightSidebarOpen() { - return Template.instance().data.state.get('rightSidebar'); - }, +// Define a handler for success on image load +const loadSuccess = function(image, studyInstanceUid, displaySetInstanceUid) { + // This is an off-screen canvas. It's used to get dataURL images by using + // cornerstone.renderToCanvas function. + const canvasElement = document.createElement('canvas'); + canvasElement.width = 193; + canvasElement.height = 123; - ViewerMain() { - return Viewerbase.components.viewer.ViewerMain; + console.warn(this.state); + + // Render the image to canvas to be able to get its dataURL + renderAsync(canvasElement, image).then(() => { + const studies = this.state.studiesForBrowser; + const study = studies.find(study => study.studyInstanceUid === studyInstanceUid); + const thumbnail = study.thumbnails.find(t => t.displaySetInstanceUid === displaySetInstanceUid); + + thumbnail.imageSrc = canvasElement.toDataURL('image/jpeg', 1); + + console.warn(this.state); + + this.setState({ + studiesForBrowser: studies + }); + }); +}; + +// Define a handler for error on image load +const loadError = function(error, studyInstanceUid, displaySetInstanceUid) { + const studies = this.state.studiesForBrowser; + const study = studies.find(study => study.studyInstanceUid === studyInstanceUid); + const thumbnail = study.thumbnails.find(t => t.displaySetInstanceUid === displaySetInstanceUid); + + thumbnail.error = error; + + this.setState({ + studiesForBrowser: studies + }); +}; + +const ViewerMain = Viewerbase.components.viewer.ViewerMain; + +class FlexboxLayout extends Component { + constructor(props) { + super(props); + + this.state = { + leftSidebarOpen: true, + rightSidebarOpen: false, + studiesForBrowser: this.getStudiesForBrowser(), + }; + + this.getStudiesForBrowser = this.getStudiesForBrowser.bind(this); + this.getThumbnailsFromImageIds = this.getThumbnailsFromImageIds.bind(this); } -}); + + componentDidMount() { + this.getThumbnailsFromImageIds(); + } + + getThumbnailsFromImageIds() { + const studyThumbnails = this.state.studiesForBrowser.forEach(function (study) { + const { studyInstanceUid } = study; + + study.thumbnails.forEach(function (displaySet) { + const imageId = displaySet.imageId; + const {displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames} = displaySet; + + cornerstone.loadAndCacheImage(imageId).then((image) => { + loadSuccess.call(this, image, studyInstanceUid, displaySetInstanceUid); + }, error => { + loadError.call(this, error, studyInstanceUid, displaySetInstanceUid); + }); + }, this); + }, this); + + } + + getStudiesForBrowser() { + // @TypeSafeStudies + const studies = OHIF.viewer.Studies.findAllBy({ + selected: true + }); + + + // TODO[react]: + // - Add sorting of display sets + // - Pass in which display set is active from Redux + // - Pass in errors and stack loading progress from Redux + // - Add useMiddleSeriesInstanceAsThumbnail + // - Add showStackLoadingProgressBar option + return studies.map((study) => { + const { studyInstanceUid } = study; + + const thumbnails = study.displaySets.map((displaySet) => { + const active = false; + const stackPercentComplete = 0; + const { displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames } = displaySet; + const imageId = displaySet.images[0].getImageId(); + return { + imageSrc: '', + imageId, + displaySetInstanceUid, + seriesDescription, + seriesNumber, + instanceNumber, + numImageFrames, + active, + stackPercentComplete + }; + }); + + return { + studyInstanceUid, + thumbnails + }; + }); + } + + render() { + let mainContentClassName = "mainContent" + if (this.state.leftSidebarOpen) { + mainContentClassName += ' sidebar-left-open'; + } + + if (this.state.rightSidebarOpen) { + mainContentClassName += ' sidebar-right-open'; + } + + // TODO[react]: Add measurementLightTable + return ( + + + + + + + + + {/*{{> measurementLightTable (clone this)}}*/} + + + ); + } +} + +FlexboxLayout.propTypes = { + studies: PropTypes.array.isRequired +}; + +export default FlexboxLayout; diff --git a/OHIFViewer/client/components/viewer/viewer.html b/OHIFViewer/client/components/viewer/viewer.html index d2db79ea1..e413c6151 100644 --- a/OHIFViewer/client/components/viewer/viewer.html +++ b/OHIFViewer/client/components/viewer/viewer.html @@ -1,13 +1,19 @@ {{#if and Template.subscriptionsReady}} + + {{> React component=CineDialog}} + - {{>cineDialog}} {{>layoutChooser}} {{>annotationDialogs}} - {{>toolbarSection (clone this state=state)}} - {{>flexboxLayout (clone this state=state)}} + + {{>toolbarSection (clone this state=state)}} + + + {{> React component=FlexboxLayout studies=this.studies}} + {{else}} {{>loadingText}} diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index d38db29e0..251df3cc8 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -6,6 +6,9 @@ import { Tracker } from 'meteor/tracker'; import { OHIF } from 'meteor/ohif:core'; import { MeasurementTable } from 'meteor/ohif:measurement-table'; +import { CineDialog } from 'react-viewerbase'; +import FlexboxLayout from '../flexboxLayout/flexboxLayout.js'; + import 'meteor/ohif:cornerstone'; import 'meteor/ohif:viewerbase'; import 'meteor/ohif:metadata'; @@ -180,5 +183,13 @@ Template.viewer.onDestroyed(function() { Template.viewer.helpers({ state() { return Template.instance().state; + }, + + CineDialog() { + return CineDialog + }, + + FlexboxLayout() { + return FlexboxLayout } }); diff --git a/OHIFViewer/package-lock.json b/OHIFViewer/package-lock.json index 308c2004d..195f8d6a1 100644 --- a/OHIFViewer/package-lock.json +++ b/OHIFViewer/package-lock.json @@ -49,6 +49,11 @@ "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", "dev": true }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -413,6 +418,13 @@ "warning": "^4.0.1" } }, + "react-viewerbase": { + "version": "git://github.com/OHIF/react-viewerbase.git#af26417b7c1da75b713f70b38c9dc3ce29e5c359", + "from": "git://github.com/OHIF/react-viewerbase.git", + "requires": { + "classnames": "^2.2.6" + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", diff --git a/OHIFViewer/package.json b/OHIFViewer/package.json index 7ab013e9b..67142372a 100644 --- a/OHIFViewer/package.json +++ b/OHIFViewer/package.json @@ -12,7 +12,8 @@ "react": "^16.6.3", "react-dom": "^16.6.3", "react-router-dom": "^4.3.1", - "redux": "^4.0.1" + "redux": "^4.0.1", + "react-viewerbase": "git://github.com:OHIF/react-viewerbase.git" }, "devDependencies": { "puppeteer": "^1.5.0" diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.html b/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.html deleted file mode 100644 index 16b7dab1f..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - {{#each study in studies}} - {{#each thumbnail in (sort (studyThumbnails study) thumbnailSortBy thumbnailSortType)}} - {{>thumbnailEntry thumbnail=thumbnail}} - {{/each}} - {{/each}} - - - diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.js b/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.js deleted file mode 100644 index a45226d3e..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Template } from 'meteor/templating'; -import { OHIF } from 'meteor/ohif:core'; - -Template.studyBrowser.helpers({ - studies() { - // @TypeSafeStudies - return OHIF.viewer.Studies.findAllBy({ - selected: true - }); - } -}); diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.styl b/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.styl deleted file mode 100644 index 856a833f3..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/studyBrowser/studyBrowser.styl +++ /dev/null @@ -1,19 +0,0 @@ -.studyBrowser - float: left - height: 100% - width: 100% - overflow: hidden - background-color: black - padding-bottom: 20px - - .scrollableStudyThumbnails - height: 100% - overflow-y: auto - overflow-x: hidden - padding-bottom: 50px - padding-right: 16px - padding-left: 4px - margin-right: -16px - - &::-webkit-scrollbar - display: none \ No newline at end of file diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html b/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html deleted file mode 100644 index 5b61401bc..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html +++ /dev/null @@ -1,30 +0,0 @@ - - {{#let stack=this.thumbnail.stack}} - - - {{>imageThumbnail thumbnail=thumbnail currentStudy=currentStudy}} - - - - {{stack.seriesDescription}} - - - - S: - {{stack.seriesNumber}} - - {{#unless isUndefined instanceNumber}} - - I: - {{instanceNumber}} - - {{/unless}} - - - {{stack.numImageFrames}} - - - - - {{/let}} - diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js b/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js deleted file mode 100644 index ffdef25b0..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js +++ /dev/null @@ -1,105 +0,0 @@ -import { Template } from 'meteor/templating'; -import { _ } from 'meteor/underscore'; -import { Session } from 'meteor/session'; - -import { OHIF } from 'meteor/ohif:core'; -import { thumbnailDragHandlers } from '../../../lib/thumbnailDragHandlers'; - -Template.thumbnailEntry.onCreated(() => { - const instance = Template.instance(); - - // Check if the thumbnails will be draggable or clickable - const isIndexUndefined = _.isUndefined(instance.data.viewportIndex); - instance.isDragAndDrop = isIndexUndefined && OHIF.uiSettings.leftSidebarDragAndDrop !== false; -}); - -Template.thumbnailEntry.events({ - // Event handlers for drag and drop - 'mousedown .thumbnailEntry'(event, instance) { - const data = instance.data.thumbnail.stack; - if (!instance.isDragAndDrop || event.button !== 0) return; - thumbnailDragHandlers.thumbnailDragStartHandler(event, data); - }, - - 'touchstart .thumbnailEntry'(event, instance) { - const data = instance.data.thumbnail.stack; - if (!instance.isDragAndDrop) return; - thumbnailDragHandlers.thumbnailDragStartHandler(event, data); - }, - - 'touchmove .thumbnailEntry'(event, instance) { - if (!instance.isDragAndDrop) return; - thumbnailDragHandlers.thumbnailDragHandler(event); - }, - - 'touchend .thumbnailEntry'(event, instance) { - const data = instance.data.thumbnail.stack; - if (!instance.isDragAndDrop) return; - thumbnailDragHandlers.thumbnailDragEndHandler(event, data); - }, - - // Event handlers for click (quick switch) - 'click .thumbnailEntry'(event, instance) { - if (instance.isDragAndDrop) return; - - // Get the thumbnail stack data - const data = instance.data.thumbnail.stack; - - // Get the viewport index - let { viewportIndex } = instance.data; - if (_.isUndefined(viewportIndex)) { - viewportIndex = window.store.getState().viewports.activeViewport || 0; - } - - // Rerender the viewport using the clicked thumbnail data - OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data); - }, - - // Event handlers for double click - 'dblclick .thumbnailEntry'(event, instance) { - if (!instance.isDragAndDrop) return; - - // Get the active viewport index and total number of viewports... - const viewportCount = OHIF.viewerbase.layoutManager.getNumberOfViewports(); - let viewportIndex = window.store.getState().viewports.activeViewport || 0; - if (viewportIndex >= viewportCount) { - viewportIndex = viewportCount > 0 ? viewportCount - 1 : 0; - } - - // Get the thumbnail stack data - const data = instance.data.thumbnail.stack; - - // Rerender the viewport using the clicked thumbnail data - OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data); - } -}); - -Template.thumbnailEntry.helpers({ - draggableClass() { - return Template.instance().isDragAndDrop ? 'draggable' : ''; - }, - - instanceNumber() { - const thumbnail = Template.instance().data.thumbnail; - if (!thumbnail) { - return; - } - - const stack = thumbnail.stack; - if (!stack) { - return; - } - - // No need to show instance number for single-frame images - if (!stack.isMultiFrame) { - return; - } - - const firstImage = stack.images[0]; - if (!firstImage) { - return; - } - - return firstImage.instanceNumber; - } -}); diff --git a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl b/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl deleted file mode 100644 index 2f796a166..000000000 --- a/Packages/ohif-viewerbase/client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl +++ /dev/null @@ -1,95 +0,0 @@ -@import "{ohif:design}/app" - -$seriesCountBackgroundColor = #678696 - -.thumbnailEntry - cursor: pointer - display: table - margin: 0 auto - - &.draggable - cursor: copy - cursor: -webkit-grab - cursor: -moz-grab - - .seriesDetails - theme('color', '$textPrimaryColor') - font-size: 14px - line-height: 1.3em - margin-top: 5px - max-width: 217px - min-height: 36px - position: relative - word-wrap: break-word - - &.info-only - - .seriesDescription - display: none - - .seriesInformation - display: flex - flex-grow: 1 - float: none - max-width: none - padding-right: 0 - - .item - flex: 1 - text-align: center - - .icon, .value - display: inline - float: none - line-height: 25px - - .value - margin-left: 0 - width: auto - - .seriesInformation - padding-right: 4px - max-width: 50px - - .item-frames .icon - height: 18px - - .value - theme('color', '$textSecondaryColor') - display: inline-block - float: right - font-size: 12px - margin-left: 4px - overflow: hidden - text-overflow: ellipsis - white-space: nowrap - width: calc(100% - 15px) - - .icon - theme('color', '$activeColor') - display: inline-block - float: left - font-size: 10px - font-weight: 900 - text-align: right - width: 11px - - div - background-color: $seriesCountBackgroundColor - margin-top: 6px - position: relative - - &:after - theme('background-color', '$activeColor') - box-shadow: 1px 1px rgba(0, 0, 0, .115) - left: -5px - position: absolute - top: -5px - - & - &:after - theme('border', '1px solid $primaryBackgroundColor') - content: '' - display: inline-block - height: 11px - width: 11px diff --git a/Packages/ohif-viewerbase/package.js b/Packages/ohif-viewerbase/package.js index 3d6cc4b9a..12030e3ea 100644 --- a/Packages/ohif-viewerbase/package.js +++ b/Packages/ohif-viewerbase/package.js @@ -6,6 +6,7 @@ Package.describe({ Npm.depends({ 'react-cornerstone-viewport': 'git://github.com:cornerstonejs/react-cornerstone-viewport.git', + 'react-viewerbase': 'git://github.com:OHIF/react-viewerbase.git', hammerjs: '2.0.8', 'cornerstone-core': '2.2.8', 'cornerstone-tools': '3.0.0-b.1471', @@ -118,19 +119,6 @@ Package.onUse(function(api) { api.addFiles('client/components/basic/aboutModal/aboutModal.js', 'client'); api.addFiles('client/components/basic/aboutModal/aboutModal.styl', 'client'); - // Study Browser components - api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.html', 'client'); - api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.js', 'client'); - api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.styl', 'client'); - - api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html', 'client'); - api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js', 'client'); - api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl', 'client'); - - api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.html', 'client'); - api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.js', 'client'); - api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.styl', 'client'); - // Viewer components api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.html', 'client'); api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.js', 'client');