Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3ce909434 | ||
|
|
ad2c750503 | ||
|
|
cceb61f123 | ||
|
|
13bcba8c39 | ||
|
|
d2db026158 | ||
|
|
e4b0552db4 | ||
|
|
d7ba2fac69 | ||
|
|
761d54fbbf | ||
|
|
79548d2380 | ||
|
|
6a401c9e52 | ||
|
|
23376d279d | ||
|
|
4110b0422c | ||
|
|
ed501bb462 | ||
|
|
abf050e162 | ||
|
|
35ed82e161 | ||
|
|
d0ccc22fd9 | ||
|
|
31841c495f | ||
|
|
65505d9bce | ||
|
|
460fdeb534 | ||
|
|
f4b7091ae4 | ||
|
|
1e72684455 | ||
|
|
5b0b6301b9 | ||
|
|
03a34a75a1 | ||
|
|
729272a985 | ||
|
|
9194ec2404 | ||
|
|
52c95a1209 | ||
|
|
8903adaea7 | ||
|
|
9481e2a99e | ||
|
|
f783fd7650 | ||
|
|
79afee7615 | ||
|
|
539141fe5a | ||
|
|
a046c6c901 | ||
|
|
073be065cf | ||
|
|
252536a1e6 | ||
|
|
53b1d71d56 | ||
|
|
702d99cb93 | ||
|
|
bb7b78bf33 | ||
|
|
b796fc9c62 | ||
|
|
66c587b139 | ||
|
|
f6e2e58aad | ||
|
|
489d4b0584 | ||
|
|
36019156da | ||
|
|
f2a371fc2d | ||
|
|
ad2f038817 | ||
|
|
8f2604509d | ||
|
|
ea0963d75f | ||
|
|
46d5718011 | ||
|
|
f8740e04f3 | ||
|
|
669717d27b | ||
|
|
715dfc556a | ||
|
|
cafb7d3f7b | ||
|
|
5f92f6ec7f | ||
|
|
eea610dfe8 | ||
|
|
e8733a9bd9 | ||
|
|
d292931073 | ||
|
|
f6a0db753a | ||
|
|
8518e179ee | ||
|
|
3f40cdced7 | ||
|
|
4cf17185da | ||
|
|
b8ae92c2d4 | ||
|
|
bad67020cc | ||
|
|
2afcd498f3 | ||
|
|
ce4c3d5360 | ||
|
|
d575e36879 | ||
|
|
98fcabfcb5 | ||
|
|
1e2773345a | ||
|
|
f847633cf3 | ||
|
|
bd2ddd7702 | ||
|
|
26b7b34bd4 | ||
|
|
6f3e446a79 | ||
|
|
f0d7f2e6c7 | ||
|
|
c8f7abfd53 | ||
|
|
2b29ff1820 | ||
|
|
5627e9c919 | ||
|
|
582b713b84 | ||
|
|
05444f48d4 | ||
|
|
2ea227ca06 | ||
|
|
35aaad175c | ||
|
|
863fe05907 | ||
|
|
1c9560044f |
No files matched your search
@@ -119,6 +119,7 @@ jobs:
|
||||
command: yarn install --frozen-lockfile
|
||||
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
docker_layer_caching: false
|
||||
|
||||
- run:
|
||||
@@ -295,6 +296,7 @@ jobs:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
custom: https://giving.massgeneral.org/ohif
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: "\U0001F917 Support Question"
|
||||
about: "I have a question \U0001F4AC"
|
||||
title: ''
|
||||
labels: 'Community: Question :question:, Triage :white_flag:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
> :hand: We are a small team with limited resources. Your question is much more
|
||||
> likely to be answered if it is
|
||||
> [a good question](https://stackoverflow.com/help/how-to-ask)
|
||||
|
||||
**Description**
|
||||
|
||||
Questions can often be answered by our documentation. Unable to find an answer
|
||||
in our docs? We'll try to help. In the meantime, if you answer your own
|
||||
question, please respond with the answer here so that others may benefit as
|
||||
well. Better yet, open a PR to expand our docs ^\_^
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤗 Support Question
|
||||
url: https://community.ohif.org/
|
||||
about: Please use our forum if you have questions or need help.
|
||||
@@ -9,6 +9,10 @@ src/version.js
|
||||
junit.xml
|
||||
coverage/
|
||||
.docz/
|
||||
*.js.map
|
||||
.yarn
|
||||
platform/docs/.docusaurus
|
||||
platform/ui/storybook-static
|
||||
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
@@ -34,3 +38,4 @@ screenshots/
|
||||
|
||||
# Locize settings
|
||||
.locize
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
cff-version: 1.2.0
|
||||
message: "If you use this software, please cite it as below."
|
||||
authors:
|
||||
- family-names: "Ziegler"
|
||||
given-names: "Erik"
|
||||
orcid: "https://orcid.org/0000-0003-1857-8129"
|
||||
- family-names: "Urban"
|
||||
given-names: "Trinity"
|
||||
- family-names: "Brown"
|
||||
given-names: "Danny"
|
||||
- family-names: "Petts"
|
||||
given-names: "James"
|
||||
- family-names: "Pieper"
|
||||
given-names: "Steve D"
|
||||
- family-names: "Lewis"
|
||||
given-names: "Rob"
|
||||
- family-names: "Hafey"
|
||||
given-names: "Chris"
|
||||
- family-names: "Harris"
|
||||
given-names: "Gordon J"
|
||||
title: "Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework for Building Web-Based Imaging Applications to Support Cancer Research"
|
||||
doi: 10.1200/CCI.19.00131
|
||||
version: 1.0.0
|
||||
url: "https://github.com/OHIF/Viewers"
|
||||
+1
-1
@@ -53,7 +53,7 @@ RUN yarn run build
|
||||
|
||||
# Stage 2: Bundle the built application into a Docker container
|
||||
# which runs Nginx using Alpine Linux
|
||||
FROM nginx:1.15.5-alpine
|
||||
FROM nginx:1.21.1-alpine
|
||||
RUN apk add --no-cache bash
|
||||
RUN rm -rf /etc/nginx/conf.d
|
||||
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
||||
|
||||
@@ -273,6 +273,17 @@ development of OHIF Viewer:
|
||||
Leidos Biomedical Research under Task Order HHSN26100071 from NCI.
|
||||
[IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized
|
||||
version of the OHIF Viewer.
|
||||
|
||||
### Research notice
|
||||
Please note that this repository is participating in a study into sustainability
|
||||
of open source projects. Data will be gathered about this repository for
|
||||
approximately the next 12 months, starting from June 2021.
|
||||
|
||||
Data collected will include number of contributors, number of PRs, time taken to
|
||||
close/merge these PRs, and issues closed.
|
||||
|
||||
For more information, please visit
|
||||
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -67,6 +67,33 @@ window.config = {
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
/**
|
||||
* Disable caching of servers configuration.
|
||||
*
|
||||
* There will be no effect if you update the servers property of this config
|
||||
* while your application is running because this property is cached in local storage.
|
||||
*/
|
||||
disableServersCache: false,
|
||||
/**
|
||||
* OHIF's study prefetcher configuration.
|
||||
*
|
||||
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
|
||||
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
|
||||
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
|
||||
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
|
||||
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
|
||||
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
|
||||
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
|
||||
*/
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'all',
|
||||
displaySetCount: 1,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
displayProgress: false,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
@@ -81,7 +108,14 @@ window.config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Supported Keys: https://craig.is/killing/mice
|
||||
/**
|
||||
* Hotkey definitions.
|
||||
* Supported Keys: https://craig.is/killing/mice
|
||||
*
|
||||
* @param {string} commandName
|
||||
* @param {string} label
|
||||
* @param {array} keys
|
||||
*/
|
||||
hotkeys: [
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
@@ -97,14 +131,16 @@ window.config = {
|
||||
keys: ['v'],
|
||||
},
|
||||
],
|
||||
/* Configuration passed to the bundled cornerstone extension
|
||||
/**
|
||||
* Configuration passed to the bundled cornerstone extension
|
||||
*
|
||||
* The cornerstone extension is currently tightly coupled to the platform.
|
||||
* Until we're able to decouple it, this key will serve as a workaround to
|
||||
* pass it configuration.
|
||||
*
|
||||
* @param {boolean} hideHandles Whether to show/hide annotation "handles"
|
||||
*/
|
||||
cornerstoneExtensionConfig: {
|
||||
/* Whether to show/hide annotation "handles" */
|
||||
hideHandles: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Viewer: Study Prefetcher & Stack Prefetch
|
||||
|
||||
Prefetching the stack of images of the active viewport is enabled by default for
|
||||
cornerstone viewports via cornerstone tools's stack prefetch functionality.
|
||||
|
||||
In order to have a more customizable alternative for prefetching, OHIF provides
|
||||
a built-in study prefetcher functionality which allows the user to set the order
|
||||
in which the display sets are prefetched, the granularity, the timeout and
|
||||
whether or not display a progress bar in the display set (close to the thumbnail
|
||||
image) in the study browser.
|
||||
|
||||
You can customize these options via the viewer's configuration:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/**
|
||||
* OHIF's study prefetcher configuration.
|
||||
*
|
||||
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
|
||||
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
|
||||
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
|
||||
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
|
||||
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
|
||||
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
|
||||
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
|
||||
*/
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'all',
|
||||
displaySetCount: 1,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
displayProgress: false,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -3,6 +3,71 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.3...@ohif/extension-cornerstone@2.12.4) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.2...@ohif/extension-cornerstone@2.12.3) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.1...@ohif/extension-cornerstone@2.12.2) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.0...@ohif/extension-cornerstone@2.12.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.11.0...@ohif/extension-cornerstone@2.12.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.10...@ohif/extension-cornerstone@2.11.0) (2021-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.9...@ohif/extension-cornerstone@2.10.10) (2021-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* changed new image handler conditional for 0 edge case ([#2338](https://github.com/OHIF/Viewers/issues/2338)) ([b8ae92c](https://github.com/OHIF/Viewers/commit/b8ae92c2d4f733b3ca89826118029685650b396a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.8...@ohif/extension-cornerstone@2.10.9) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -85,12 +85,32 @@ Tools can be configured through extension configuration using the tools key:
|
||||
...
|
||||
```
|
||||
|
||||
## Stack prefetch configuration
|
||||
|
||||
Stack prefetch can be configured through extension configuration using the
|
||||
stackPrefetch key:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
stackPrefetch: {
|
||||
enabled: true,
|
||||
maxImagesToPrefetch: Infinity,
|
||||
preserveExistingPool: false,
|
||||
maxSimultaneousRequests: 20,
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Annotate Tools Configuration
|
||||
|
||||
*We currently support one property for annotation tools.*
|
||||
_We currently support one property for annotation tools._
|
||||
|
||||
### Hide handles
|
||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
||||
|
||||
This extension configuration allows you to toggle on/off handle rendering for
|
||||
all annotate tools:
|
||||
|
||||
```js
|
||||
...
|
||||
@@ -119,3 +139,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
|
||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
||||
<!-- prettier-ignore-end -->
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.10.9",
|
||||
"version": "2.12.4",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,12 +32,12 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-math": "^0.1.9",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dcmjs": "0.19.2",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
@@ -51,6 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-cornerstone-viewport": "4.0.5"
|
||||
"react-cornerstone-viewport": "4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
// Currently justing using escape hatch + commands
|
||||
// activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
isStackPrefetchEnabled: isActive,
|
||||
isStackPrefetchEnabled: ownProps.hasOwnProperty('isStackPrefetchEnabled')
|
||||
? ownProps.isStackPrefetchEnabled
|
||||
: ownProps.stackPrefetch
|
||||
? ownProps.stackPrefetch.enabled
|
||||
: isActive,
|
||||
isPlaying,
|
||||
frameRate,
|
||||
//stack: viewportSpecificData.stack,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay'
|
||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay';
|
||||
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -15,6 +15,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
customProps: {},
|
||||
isStackPrefetchEnabled: true,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -23,6 +24,8 @@ class OHIFCornerstoneViewport extends Component {
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
stackPrefetch: PropTypes.object,
|
||||
isStackPrefetchEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static id = 'OHIFCornerstoneViewport';
|
||||
@@ -221,7 +224,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
|
||||
if (currentImageIdIndex > 0) {
|
||||
if (currentImageIdIndex >= 0) {
|
||||
this.props.onNewImage({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
@@ -232,7 +235,12 @@ class OHIFCornerstoneViewport extends Component {
|
||||
};
|
||||
|
||||
const warningsOverlay = props => {
|
||||
return <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
|
||||
return (
|
||||
<OHIFCornerstoneViewportOverlay
|
||||
{...props}
|
||||
inconsistencyWarnings={inconsistencyWarnings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -244,10 +252,11 @@ class OHIFCornerstoneViewport extends Component {
|
||||
onNewImageDebounced={newImageHandler}
|
||||
onNewImageDebounceTime={300}
|
||||
viewportOverlayComponent={warningsOverlay}
|
||||
stackPrefetch={this.props.stackPrefetch}
|
||||
isStackPrefetchEnabled={this.props.isStackPrefetchEnabled}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
// isStackPrefetchEnabled={true}
|
||||
// onElementEnabled={() => {}}
|
||||
// setViewportActive{() => {}}
|
||||
{...this.props.customProps}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getViewportModule({ commandsManager }) {
|
||||
getViewportModule({ commandsManager, appConfig }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
/**
|
||||
* TODO: This appears to be used to set the redux parameters for
|
||||
@@ -48,8 +48,17 @@ export default {
|
||||
jumpData.refreshViewports = false;
|
||||
commandsManager.runCommand('jumpToImage', jumpData);
|
||||
};
|
||||
|
||||
const { studyPrefetcher } = appConfig;
|
||||
const isStackPrefetchEnabled =
|
||||
studyPrefetcher && !studyPrefetcher.enabled;
|
||||
|
||||
return (
|
||||
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
onNewImage={onNewImageHandler}
|
||||
isStackPrefetchEnabled={isStackPrefetchEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,14 @@ export default function init({ servicesManager, configuration }) {
|
||||
}
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const {
|
||||
csToolsConfig,
|
||||
stackPrefetch = {
|
||||
maxImagesToPrefetch: Infinity,
|
||||
preserveExistingPool: false,
|
||||
maxSimultaneousRequests: 20,
|
||||
},
|
||||
} = configuration;
|
||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
@@ -52,7 +59,7 @@ export default function init({ servicesManager, configuration }) {
|
||||
autoResizeViewports: false,
|
||||
};
|
||||
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
initCornerstoneTools({ ...defaultCsToolsConfig, ...stackPrefetch });
|
||||
|
||||
const toolsGroupedByType = {
|
||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||
|
||||
@@ -37,8 +37,8 @@ export default function(configuration = {}) {
|
||||
|
||||
// Configure stack prefetch
|
||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
||||
maxImagesToPrefetch: Infinity,
|
||||
preserveExistingPool: false,
|
||||
maxSimultaneousRequests: 20,
|
||||
maxImagesToPrefetch: configuration.maxImagesToPrefetch,
|
||||
preserveExistingPool: configuration.preserveExistingPool,
|
||||
maxSimultaneousRequests: configuration.maxSimultaneousRequests,
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.0...@ohif/extension-debugging@0.3.1) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.2...@ohif/extension-debugging@0.3.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.1...@ohif/extension-debugging@0.2.2) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.0...@ohif/extension-debugging@0.2.1) (2021-03-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-debugging",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.1",
|
||||
"description": "OHIF extension for debugging.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,13 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.5.2"
|
||||
"dicom-parser": "^1.8.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"detect-browser": "5.1.1",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.14...@ohif/extension-dicom-html@1.3.15) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.13...@ohif/extension-dicom-html@1.3.14) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.12...@ohif/extension-dicom-html@1.3.13) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.11...@ohif/extension-dicom-html@1.3.12) (2021-10-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.10...@ohif/extension-dicom-html@1.3.11) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.9...@ohif/extension-dicom-html@1.3.10) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dcmjs": "0.19.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -160,7 +160,7 @@ const getContentSequence = (data, level = 1) => {
|
||||
|
||||
const root = [];
|
||||
if (header) {
|
||||
const HeaderDynamicLevel = `h${level}`;
|
||||
const HeaderDynamicLevel = `h${Math.min(level, 6)}`;
|
||||
|
||||
root.push(<HeaderDynamicLevel key={header}>{header}</HeaderDynamicLevel>);
|
||||
}
|
||||
@@ -170,7 +170,7 @@ const getContentSequence = (data, level = 1) => {
|
||||
|
||||
if (key === '_meta') {
|
||||
const HeaderDynamicLevel = `h3`;
|
||||
root.push(<hr />);
|
||||
root.push(<hr key={root.length} />);
|
||||
root.push(
|
||||
<HeaderDynamicLevel key="Metadata">
|
||||
DICOM File Meta Information
|
||||
@@ -217,7 +217,7 @@ class DicomHtmlViewport extends Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const dataSet = this.setContentFromByteArray(this.props.byteArray);
|
||||
this.setContentFromByteArray(this.props.byteArray);
|
||||
}
|
||||
|
||||
setContentFromByteArray(byteArray) {
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.1.0...@ohif/extension-dicom-pdf@1.1.1) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.6...@ohif/extension-dicom-pdf@1.1.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.5...@ohif/extension-dicom-pdf@1.0.6) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.6",
|
||||
"version": "1.1.1",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -3,6 +3,73 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.5...@ohif/extension-dicom-rt@0.7.6) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.4...@ohif/extension-dicom-rt@0.7.5) (2022-01-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.3...@ohif/extension-dicom-rt@0.7.4) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.2...@ohif/extension-dicom-rt@0.7.3) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.1...@ohif/extension-dicom-rt@0.7.2) (2021-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.0...@ohif/extension-dicom-rt@0.7.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.14...@ohif/extension-dicom-rt@0.7.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.13...@ohif/extension-dicom-rt@0.6.14) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.12...@ohif/extension-dicom-rt@0.6.13) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.6.13",
|
||||
"version": "0.7.6",
|
||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,9 +29,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.8",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"dcmjs": "0.19.2",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
|
||||
@@ -81,13 +81,10 @@ const RTPanel = ({
|
||||
useEffect(() => {
|
||||
updateStructureSets();
|
||||
|
||||
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
document.addEventListener('rt-panel-tab-updated', updateStructureSets);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
'extensiondicomrtrtloaded',
|
||||
updateStructureSets
|
||||
);
|
||||
document.removeEventListener('rt-panel-tab-updated', updateStructureSets);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import init from './init.js';
|
||||
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||
import id from './id.js';
|
||||
import RTPanel from './components/RTPanel/RTPanel';
|
||||
import { version } from '../package.json';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
export default {
|
||||
@@ -42,29 +43,60 @@ export default {
|
||||
);
|
||||
};
|
||||
|
||||
const RTPanelTabChangedEvent = 'rt-panel-tab-updated';
|
||||
|
||||
/**
|
||||
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
|
||||
*
|
||||
* This is required to avoid extension state
|
||||
* coupling with the viewer's ToolbarRow component.
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
const triggerRTPanelUpdatedEvent = data => {
|
||||
const event = new CustomEvent(RTPanelTabChangedEvent, {
|
||||
detail: data,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onRTStructsLoaded = ({ detail }) => {
|
||||
const { rtStructDisplaySet, referencedDisplaySet } = detail;
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
rtStructDisplaySet.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: referencedDisplaySet.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
triggerRTPanelUpdatedEvent({
|
||||
badgeNumber: referencedDisplaysets.length,
|
||||
target: 'rt-panel',
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('extensiondicomrtrtloaded', onRTStructsLoaded);
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'RTSTRUCT',
|
||||
target: 'rt-panel',
|
||||
stateEvent: RTPanelTabChangedEvent,
|
||||
isDisabled: (studies, activeViewport) => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activeViewport) {
|
||||
const study = studies.find(
|
||||
s => s.StudyInstanceUID === activeViewport.StudyInstanceUID
|
||||
);
|
||||
const ds = study.displaySets.find(
|
||||
ds =>
|
||||
ds.displaySetInstanceUID ===
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
if (!studyMetadata) {
|
||||
return;
|
||||
}
|
||||
const referencedDisplaySets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
@@ -75,6 +107,10 @@ export default {
|
||||
['RTSTRUCT'].includes(ds.Modality)
|
||||
)
|
||||
) {
|
||||
triggerRTPanelUpdatedEvent({
|
||||
badgeNumber: referencedDisplaySets.length,
|
||||
target: 'rt-panel',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ export default async function loadRTStruct(
|
||||
const sopInstanceUID = ContourImageSequence
|
||||
? ContourImageSequence.ReferencedSOPInstanceUID
|
||||
: _getClosestSOPInstanceUID(
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
);
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
);
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
|
||||
if (!imageId) {
|
||||
@@ -171,7 +171,13 @@ export default async function loadRTStruct(
|
||||
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded', {
|
||||
detail: {
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
@@ -299,7 +305,9 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
return imageIdSopInstanceUidPairsEntry
|
||||
? imageIdSopInstanceUidPairsEntry.imageId
|
||||
: null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
|
||||
@@ -3,6 +3,73 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.5...@ohif/extension-dicom-segmentation@0.7.6) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.4...@ohif/extension-dicom-segmentation@0.7.5) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.3...@ohif/extension-dicom-segmentation@0.7.4) (2021-10-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.2...@ohif/extension-dicom-segmentation@0.7.3) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.1...@ohif/extension-dicom-segmentation@0.7.2) (2021-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.0...@ohif/extension-dicom-segmentation@0.7.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.11...@ohif/extension-dicom-segmentation@0.7.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.10...@ohif/extension-dicom-segmentation@0.6.11) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.9...@ohif/extension-dicom-segmentation@0.6.10) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.6",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,9 +29,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.8",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"dcmjs": "0.19.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -24,6 +24,7 @@ const SegmentItem = ({
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
labelmap3D,
|
||||
visible,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
@@ -39,7 +40,7 @@ const SegmentItem = ({
|
||||
event.stopPropagation();
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility, index);
|
||||
onVisibilityChange(newVisibility, index, labelmap3D);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+46
-20
@@ -224,16 +224,20 @@ const SegmentationPanel = ({
|
||||
)
|
||||
);
|
||||
};
|
||||
}, [activeIndex, viewports]);
|
||||
}, [
|
||||
activeIndex,
|
||||
updateSegmentationComboBox,
|
||||
viewports,
|
||||
]);
|
||||
|
||||
const updateSegmentationComboBox = (e) => {
|
||||
const updateSegmentationComboBox = e => {
|
||||
const index = e.detail.activatedLabelmapIndex;
|
||||
if (index !== -1) {
|
||||
setState(state => ({ ...state, selectedSegmentation: index }));
|
||||
} else {
|
||||
cleanSegmentationComboBox();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cleanSegmentationComboBox = () => {
|
||||
setState(state => ({
|
||||
@@ -245,7 +249,7 @@ const SegmentationPanel = ({
|
||||
isDisabled: true,
|
||||
selectedSegmentation: -1,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSegmentations = () => {
|
||||
const activeViewport = getActiveViewport();
|
||||
@@ -309,7 +313,8 @@ const SegmentationPanel = ({
|
||||
);
|
||||
|
||||
const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter(
|
||||
(segDisplay => segDisplay.loadError !== true));
|
||||
segDisplay => segDisplay.loadError !== true
|
||||
);
|
||||
|
||||
return filteredReferencedSegDisplaysets.map((displaySet, index) => {
|
||||
const {
|
||||
@@ -426,23 +431,43 @@ const SegmentationPanel = ({
|
||||
return enabledElements[activeIndex].element;
|
||||
};
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (isVisible, segmentNumber) => {
|
||||
/** Get all labelmaps with this segmentNumber (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
});
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (
|
||||
isVisible,
|
||||
segmentNumber,
|
||||
labelmap3D
|
||||
) => {
|
||||
let segmentsHidden = [];
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
if (labelmap3D.metadata.hasOverlapping) {
|
||||
/** Get all labelmaps with this segmentNumber and that
|
||||
* are from the same series (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
const sameSeriesLabelMaps3D = labelmaps3D.filter(({ metadata }) => {
|
||||
return (
|
||||
labelmap3D.metadata.segmentationSeriesInstanceUID ===
|
||||
metadata.segmentationSeriesInstanceUID
|
||||
);
|
||||
});
|
||||
|
||||
const possibleLabelMaps3D = sameSeriesLabelMaps3D.filter(
|
||||
({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
} else {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
segmentsHidden = [...labelmap3D.segmentsHidden];
|
||||
}
|
||||
|
||||
setState(state => ({ ...state, segmentsHidden }));
|
||||
|
||||
@@ -509,6 +534,7 @@ const SegmentationPanel = ({
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
labelmap3D={labelmap3D}
|
||||
visible={!labelmap3D.segmentsHidden[segmentIndex]}
|
||||
onVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||
/>
|
||||
|
||||
@@ -64,8 +64,17 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
metadata,
|
||||
};
|
||||
|
||||
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
|
||||
segDisplaySet.getSourceDisplaySet = function(
|
||||
studies,
|
||||
activateLabelMap = true,
|
||||
onDisplaySetLoadFailureHandler
|
||||
) {
|
||||
return getSourceDisplaySet(
|
||||
studies,
|
||||
segDisplaySet,
|
||||
activateLabelMap,
|
||||
onDisplaySetLoadFailureHandler
|
||||
);
|
||||
};
|
||||
|
||||
segDisplaySet.load = async function(referencedDisplaySet, studies) {
|
||||
@@ -98,6 +107,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
if (labelmapBufferArray.length > 1) {
|
||||
let labelmapIndexes = [];
|
||||
for (let i = 0; i < labelmapBufferArray.length; ++i) {
|
||||
segMetadata.segmentationSeriesInstanceUID =
|
||||
segDisplaySet.SeriesInstanceUID;
|
||||
segMetadata.hasOverlapping = true;
|
||||
labelmapIndexes.push(
|
||||
await loadSegmentation(
|
||||
imageIds,
|
||||
@@ -118,6 +130,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
labelmapIndex = labelmapIndexes[0];
|
||||
console.warn('Overlapping segments!');
|
||||
} else {
|
||||
segMetadata.segmentationSeriesInstanceUID =
|
||||
segDisplaySet.SeriesInstanceUID;
|
||||
segMetadata.hasOverlapping = false;
|
||||
labelmapIndex = await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import init from './init.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
|
||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||
import { version } from '../package.json';
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -79,13 +82,51 @@ export default {
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentationPanelTabUpdatedEvent = 'segmentation-panel-tab-updated';
|
||||
|
||||
/**
|
||||
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
|
||||
*
|
||||
* This is required to avoid extension state
|
||||
* coupling with the viewer's ToolbarRow component.
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
const triggerSegmentationPanelTabUpdatedEvent = data => {
|
||||
const event = new CustomEvent(SegmentationPanelTabUpdatedEvent, {
|
||||
detail: data,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onSegmentationsLoaded = ({ detail }) => {
|
||||
const { segDisplaySet, segMetadata } = detail;
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
segDisplaySet.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: segMetadata.seriesInstanceUid,
|
||||
Modality: 'SEG',
|
||||
});
|
||||
triggerSegmentationPanelTabUpdatedEvent({
|
||||
badgeNumber: referencedDisplaysets.length,
|
||||
target: 'segmentation-panel',
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
onSegmentationsLoaded
|
||||
);
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-panel',
|
||||
isDisabled: studies => {
|
||||
stateEvent: SegmentationPanelTabUpdatedEvent,
|
||||
isDisabled: (studies, activeViewport) => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
@@ -98,6 +139,23 @@ export default {
|
||||
const series = study.series[j];
|
||||
|
||||
if (series.Modality === 'SEG') {
|
||||
if (activeViewport) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
if (!studyMetadata) {
|
||||
return;
|
||||
}
|
||||
const referencedDS = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID:
|
||||
activeViewport.SeriesInstanceUID,
|
||||
Modality: 'SEG',
|
||||
});
|
||||
triggerSegmentationPanelTabUpdatedEvent({
|
||||
badgeNumber: referencedDS.length,
|
||||
target: 'segmentation-panel',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,16 @@ export default async function loadSegmentation(
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
console.log('Segmentation loaded.');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded', {
|
||||
detail: {
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBuffer,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
labelmapSegments,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
return labelmapIndex;
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.16...@ohif/extension-dicom-tag-browser@0.2.17) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.15...@ohif/extension-dicom-tag-browser@0.2.16) (2021-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.14...@ohif/extension-dicom-tag-browser@0.2.15) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.13...@ohif/extension-dicom-tag-browser@0.2.14) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.12...@ohif/extension-dicom-tag-browser@0.2.13) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.11...@ohif/extension-dicom-tag-browser@0.2.12) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.10...@ohif/extension-dicom-tag-browser@0.2.11) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.9...@ohif/extension-dicom-tag-browser@0.2.10) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-tag-browser",
|
||||
"version": "0.2.10",
|
||||
"version": "0.2.17",
|
||||
"description": "OHIF extension for checking DICOM headers.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dcmjs": "0.19.2",
|
||||
"react": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"moment": "2.24.0",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
activeDisplaySetInstanceUID,
|
||||
setActiveDisplaySetInstanceUID,
|
||||
] = useState(displaySetInstanceUID);
|
||||
const [activeInstance, setActiveInstance] = useState(0);
|
||||
const [activeInstance, setActiveInstance] = useState(1);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [meta, setMeta] = useState('');
|
||||
const [instanceList, setInstanceList] = useState([]);
|
||||
@@ -51,20 +51,21 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
description: displayDate,
|
||||
onClick: () => {
|
||||
setActiveDisplaySetInstanceUID(displaySetInstanceUID);
|
||||
setActiveInstance(0);
|
||||
setActiveInstance(1);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let metadata;
|
||||
const isImageStack = activeDisplaySet instanceof ImageSet;
|
||||
const isImageStack =
|
||||
activeDisplaySet instanceof ImageSet &&
|
||||
activeDisplaySet.isModalitySupported === true;
|
||||
|
||||
let selectedInstanceValue;
|
||||
let instanceList;
|
||||
|
||||
if (isImageStack) {
|
||||
const { images } = activeDisplaySet;
|
||||
const image = images[activeInstance];
|
||||
const image = images[activeInstance - 1];
|
||||
|
||||
instanceList = images.map((image, index) => {
|
||||
const metadata = image.getData().metadata;
|
||||
@@ -91,7 +92,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
setInstanceList(instanceList);
|
||||
setDisplaySetList(newDisplaySetList);
|
||||
setIsImageStack(isImageStack);
|
||||
}, [activeDisplaySetInstanceUID, activeInstance]);
|
||||
}, [activeDisplaySetInstanceUID, activeInstance, displaySets]);
|
||||
|
||||
const selectedDisplaySetValue = displaySetList.find(
|
||||
ds => ds.value === activeDisplaySetInstanceUID
|
||||
@@ -106,7 +107,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
showValue
|
||||
step={1}
|
||||
min={1}
|
||||
max={instanceList.length - 1}
|
||||
max={instanceList.length}
|
||||
value={activeInstance}
|
||||
valueRenderer={value => <p>Instance Number: {value}</p>}
|
||||
onChange={({ target }) => {
|
||||
|
||||
@@ -3,6 +3,148 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.10...@ohif/extension-vtk@1.12.11) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.9...@ohif/extension-vtk@1.12.10) (2022-01-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.8...@ohif/extension-vtk@1.12.9) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.7...@ohif/extension-vtk@1.12.8) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.6...@ohif/extension-vtk@1.12.7) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.5...@ohif/extension-vtk@1.12.6) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.4...@ohif/extension-vtk@1.12.5) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.3...@ohif/extension-vtk@1.12.4) (2021-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.2...@ohif/extension-vtk@1.12.3) (2021-10-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.1...@ohif/extension-vtk@1.12.2) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.0...@ohif/extension-vtk@1.12.1) (2021-09-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.18...@ohif/extension-vtk@1.12.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.17...@ohif/extension-vtk@1.11.18) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.16...@ohif/extension-vtk@1.11.17) (2021-09-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.15...@ohif/extension-vtk@1.11.16) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.14...@ohif/extension-vtk@1.11.15) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.13...@ohif/extension-vtk@1.11.14) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.12...@ohif/extension-vtk@1.11.13) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.11.13",
|
||||
"version": "1.12.11",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,11 +32,11 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/i18n": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dcmjs": "0.19.2",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
@@ -50,14 +50,14 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.14.2"
|
||||
"react-vtkjs-viewport": "^0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.13.12",
|
||||
"@ohif/ui": "^1.9.2",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"@ohif/core": "^2.16.7",
|
||||
"@ohif/ui": "^1.10.5",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"gh-pages": "^2.0.1",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@@ -72,7 +72,7 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
onScroll: () => { },
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -271,7 +271,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
volumeMapper.setSampleDistance(sampleDistance);
|
||||
|
||||
// Be generous to surpress warnings, as the logging really hurts performance.
|
||||
// Be generous to suppress warnings, as the logging really hurts performance.
|
||||
// TODO: maybe we should auto adjust samples to 1000.
|
||||
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||
|
||||
@@ -398,7 +398,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
|
||||
@@ -3,6 +3,144 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.16.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.6...@ohif/core@2.16.7) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.5...@ohif/core@2.16.6) (2022-01-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.4...@ohif/core@2.16.5) (2021-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DICOMFileLoadingListener typo in method name ([#2647](https://github.com/OHIF/Viewers/issues/2647)) ([761d54f](https://github.com/OHIF/Viewers/commit/761d54fbbfb376c20ee15794ef7bb7111f64a06f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.3...@ohif/core@2.16.4) (2021-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SeriesTime and SeriesDate are only set for the first series and undefined for the remaining ones ([#2476](https://github.com/OHIF/Viewers/issues/2476)) ([6a401c9](https://github.com/OHIF/Viewers/commit/6a401c9e5204cb4f7af6aa33190ad5a8d0879de6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.2...@ohif/core@2.16.3) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.1...@ohif/core@2.16.2) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.0...@ohif/core@2.16.1) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.16.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.1...@ohif/core@2.16.0) (2021-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Added support for WADO servers built with static hosting (e.g. S3) ([#2566](https://github.com/OHIF/Viewers/issues/2566)) ([a046c6c](https://github.com/OHIF/Viewers/commit/a046c6c9017a84e5ead71d5113b5e892333b4130))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.15.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.0...@ohif/core@2.15.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.15.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.1...@ohif/core@2.15.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.14.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.0...@ohif/core@2.14.1) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.14.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.15...@ohif/core@2.14.0) (2021-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.15](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.14...@ohif/core@2.13.15) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.14](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.13...@ohif/core@2.13.14) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.13](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.12...@ohif/core@2.13.13) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.11...@ohif/core@2.13.12) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.13.12",
|
||||
"version": "2.16.7",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -31,21 +31,22 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dicom-parser": "^1.8.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dcmjs": "0.19.2",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
"mousetrap": "^1.6.3",
|
||||
"retry": "^0.12.0",
|
||||
"validate.js": "^0.12.0"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||
import parseMeasurementsData from './parseMeasurementsData';
|
||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||
import errorHandler from '../errorHandler';
|
||||
import getXHRRetryRequestHook from '../utils/xhrRetryRequestHook';
|
||||
|
||||
const VERSION_NAME = 'dcmjs-0.0';
|
||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||
@@ -23,6 +24,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
@@ -74,6 +76,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import classes from '../classes';
|
||||
|
||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
|
||||
const { LogManager } = classes;
|
||||
|
||||
/**
|
||||
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
|
||||
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
|
||||
@@ -20,9 +23,21 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
);
|
||||
|
||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
||||
dataset
|
||||
);
|
||||
|
||||
let storedMeasurementByToolType;
|
||||
try {
|
||||
storedMeasurementByToolType = MeasurementReport.generateToolState(dataset);
|
||||
} catch (error) {
|
||||
const seriesDescription = dataset.SeriesDescription || '';
|
||||
LogManager.publish(LogManager.EVENTS.OnLog, {
|
||||
title: `Failed to parse ${seriesDescription} measurement report`,
|
||||
type: 'warning',
|
||||
message: error.message || '',
|
||||
notify: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const measurementData = {};
|
||||
let measurementNumber = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import PubSub from './PubSub';
|
||||
|
||||
/** Log Events */
|
||||
export const LogEvents = Object.freeze({
|
||||
OnLog: 'onLog',
|
||||
});
|
||||
|
||||
/**
|
||||
* Log manager that implements pub/sub.
|
||||
* This manager can be used to send logs across different packages
|
||||
* using previously registered events.
|
||||
*/
|
||||
class LogManager extends PubSub {
|
||||
EVENTS = LogEvents;
|
||||
}
|
||||
|
||||
/** Singleton */
|
||||
export default new LogManager();
|
||||
@@ -231,26 +231,26 @@ class MetadataProvider {
|
||||
let columnCosines;
|
||||
|
||||
if (PixelSpacing) {
|
||||
rowPixelSpacing = PixelSpacing[0];
|
||||
columnPixelSpacing = PixelSpacing[1];
|
||||
rowPixelSpacing = validNumber(PixelSpacing[0]);
|
||||
columnPixelSpacing = validNumber(PixelSpacing[1]);
|
||||
}
|
||||
|
||||
if (ImageOrientationPatient) {
|
||||
rowCosines = ImageOrientationPatient.slice(0, 3);
|
||||
columnCosines = ImageOrientationPatient.slice(3, 6);
|
||||
rowCosines = validNumber(ImageOrientationPatient.slice(0, 3));
|
||||
columnCosines = validNumber(ImageOrientationPatient.slice(3, 6));
|
||||
}
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
imageOrientationPatient: validNumber(ImageOrientationPatient),
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
imagePositionPatient: validNumber(instance.ImagePositionPatient),
|
||||
sliceThickness: validNumber(instance.SliceThickness),
|
||||
sliceLocation: validNumber(instance.SliceLocation),
|
||||
pixelSpacing: validNumber(PixelSpacing),
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
const _subscriptions = Symbol('subscriptions');
|
||||
const _lastSubscriptionId = Symbol('lastSubscriptionId');
|
||||
|
||||
/**
|
||||
* Class to implement publish/subscribe pattern
|
||||
*
|
||||
* @class
|
||||
* @classdesc Pub/sub mechanism
|
||||
*/
|
||||
export default class PubSub {
|
||||
constructor() {
|
||||
this[_subscriptions] = {};
|
||||
this[_lastSubscriptionId] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to event
|
||||
*
|
||||
* @param {string} eventName Event name
|
||||
* @param {Function} callback Callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
subscribe(eventName, callback) {
|
||||
if (eventName === undefined) {
|
||||
throw new Error('Event name is required');
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Callback must be a function');
|
||||
}
|
||||
|
||||
if (!this[_subscriptions].hasOwnProperty(eventName)) {
|
||||
this[_subscriptions][eventName] = {};
|
||||
}
|
||||
|
||||
const subscriptionId = `sub${this[_lastSubscriptionId]++}`;
|
||||
this[_subscriptions][eventName][subscriptionId] = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a subscription
|
||||
*
|
||||
* @param {string} eventName Event name
|
||||
* @param {Function} [callback] Callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
unsubscribe(eventName, callback) {
|
||||
const callbacks = this[_subscriptions][eventName] || {};
|
||||
for (let subscriptionId in callbacks) {
|
||||
if (!callback) {
|
||||
delete callbacks[subscriptionId];
|
||||
} else if (callbacks[subscriptionId] === callback) {
|
||||
delete callbacks[subscriptionId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish event to all subscriptions
|
||||
*
|
||||
* @param {String} eventName Event name
|
||||
* @param {any} [payload] Data that will be published
|
||||
* @returns {void}
|
||||
*/
|
||||
publish(eventName, ...payload) {
|
||||
if (eventName === undefined) {
|
||||
throw new Error('Event name is required');
|
||||
}
|
||||
|
||||
const callbacks = this[_subscriptions][eventName] || {};
|
||||
for (let subscriptionId in callbacks) {
|
||||
callbacks[subscriptionId](...payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import {
|
||||
clearStudyLoadingProgress,
|
||||
setStudyLoadingProgress,
|
||||
} from '../redux/actions';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import StackManager from '../utils/StackManager';
|
||||
import { StudyPrefetcher } from './StudyPrefetcher';
|
||||
|
||||
class BaseLoadingListener {
|
||||
constructor(stack, options = {}) {
|
||||
this.id = BaseLoadingListener.getNewId();
|
||||
this.stack = stack;
|
||||
this.startListening();
|
||||
this.statsItemsLimit = options.statsItemsLimit || 2;
|
||||
this.stats = {
|
||||
items: [],
|
||||
@@ -60,6 +58,9 @@ class BaseLoadingListener {
|
||||
}
|
||||
|
||||
_getProgressId() {
|
||||
/**
|
||||
* TODO: The id key should be configurable.
|
||||
*/
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
return 'StackProgress:' + displaySetInstanceUID;
|
||||
}
|
||||
@@ -96,11 +97,18 @@ class BaseLoadingListener {
|
||||
class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
constructor(stack, options) {
|
||||
super(stack, options);
|
||||
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this._dataSetUrl = this._getDataSetUrl(stack);
|
||||
this._lastLoaded = 0;
|
||||
|
||||
// Check how many instances has already been download (cached)
|
||||
this._checkCachedData();
|
||||
|
||||
this.startListening();
|
||||
}
|
||||
|
||||
_checkCachedData() {
|
||||
@@ -127,10 +135,6 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
startListening() {
|
||||
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
||||
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.stopListening();
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
@@ -200,17 +204,57 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
}
|
||||
}
|
||||
|
||||
const StudyLoadingListenerEvents = {
|
||||
OnProgress: 'StudyLoadingListenerEvents.OnProgress',
|
||||
};
|
||||
|
||||
function promiseState(promise, callback) {
|
||||
// Symbols and RegExps are never content-equal
|
||||
var uniqueValue = window['Symbol'] ? Symbol('unique') : /unique/;
|
||||
|
||||
function notifyPendingOrResolved(value) {
|
||||
if (value === uniqueValue) {
|
||||
return callback('pending');
|
||||
} else {
|
||||
return callback('fulfilled');
|
||||
}
|
||||
}
|
||||
|
||||
function notifyRejected(reason) {
|
||||
return callback('rejected');
|
||||
}
|
||||
|
||||
var race = [promise, Promise.resolve(uniqueValue)];
|
||||
Promise.race(race).then(notifyPendingOrResolved, notifyRejected);
|
||||
}
|
||||
|
||||
class StackLoadingListener extends BaseLoadingListener {
|
||||
constructor(stack, options = {}) {
|
||||
options.statsItemsLimit = 20;
|
||||
|
||||
super(stack, options);
|
||||
|
||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
|
||||
this.framesStatus = this._createArray(stack.imageIds.length, false);
|
||||
this.loadedCount = 0;
|
||||
|
||||
// Check how many instances has already been download (cached)
|
||||
this._checkCachedData();
|
||||
this._debouncedSetProgressData = debounce((...args) => {
|
||||
this._setProgressData(...args);
|
||||
|
||||
/** After checking cache, continue prefetch */
|
||||
const studyPrefetcher = StudyPrefetcher.getInstance();
|
||||
studyPrefetcher.prefetch(studyPrefetcher.getElement());
|
||||
}, 300);
|
||||
const debounced = true;
|
||||
this._checkCachedData(debounced);
|
||||
|
||||
this.startListening();
|
||||
}
|
||||
|
||||
_convertImageIdsArrayToMap(imageIds) {
|
||||
@@ -238,18 +282,37 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
return array;
|
||||
}
|
||||
|
||||
_checkCachedData() {
|
||||
// const imageIds = this.stack.imageIds;
|
||||
// TODO: No way to check status of Promise.
|
||||
/*for(let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
/**
|
||||
* Check if image id is cached in cornerstone.
|
||||
*
|
||||
* @param {string} imageId
|
||||
* @returns
|
||||
*/
|
||||
isImageCached(imageId) {
|
||||
const image = cornerstone.imageCache.imageCache[imageId];
|
||||
return image && image.sizeInBytes;
|
||||
}
|
||||
|
||||
const imagePromise = cornerstone.imageCache.getImageLoadObject(imageId).promise;
|
||||
_checkCachedData(debounced = false) {
|
||||
const imageIds = this.stack.imageIds;
|
||||
|
||||
if (imagePromise && (imagePromise.state() === 'resolved')) {
|
||||
this._updateFrameStatus(imageId, true);
|
||||
}
|
||||
}*/
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
|
||||
const imageObject = cornerstone.imageCache.getImageLoadObject(imageId);
|
||||
|
||||
if (this.isImageCached(imageId)) {
|
||||
this._updateFrameStatus(imageId, true, debounced);
|
||||
}
|
||||
|
||||
if (imageObject && imageObject.promise) {
|
||||
promiseState(imageObject.promise, state => {
|
||||
if (state === 'fulfilled') {
|
||||
this._updateFrameStatus(imageId, true, debounced);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getImageLoadedEventName() {
|
||||
@@ -272,11 +335,6 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
const imageLoadedEventName = this._getImageLoadedEventName();
|
||||
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
||||
|
||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.stopListening();
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
@@ -303,7 +361,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
);
|
||||
}
|
||||
|
||||
_updateFrameStatus(imageId, loaded) {
|
||||
_updateFrameStatus(imageId, loaded, debounced) {
|
||||
const imageData = this.imageDataMap.get(imageId);
|
||||
|
||||
if (!imageData || imageData.loaded === loaded) {
|
||||
@@ -318,7 +376,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
imageData.loaded = loaded;
|
||||
this.framesStatus[imageData.index] = loaded;
|
||||
this.loadedCount += loaded ? 1 : -1;
|
||||
this._updateProgress();
|
||||
this._updateProgress(debounced);
|
||||
}
|
||||
|
||||
_setProgressData(progressId, progressData) {
|
||||
@@ -339,7 +397,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
);
|
||||
}
|
||||
|
||||
_updateProgress() {
|
||||
_updateProgress(debounced) {
|
||||
const totalFramesCount = this.stack.imageIds.length;
|
||||
const loadedFramesCount = this.loadedCount;
|
||||
const loadingFramesCount = totalFramesCount - loadedFramesCount;
|
||||
@@ -357,6 +415,11 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
framesStatus: this.framesStatus,
|
||||
};
|
||||
|
||||
if (debounced) {
|
||||
this._debouncedSetProgressData(progressId, progressData);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setProgressData(progressId, progressData);
|
||||
}
|
||||
|
||||
@@ -371,11 +434,13 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
}
|
||||
|
||||
progressBar += ']';
|
||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
console.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
}
|
||||
}
|
||||
|
||||
class StudyLoadingListener {
|
||||
static events = StudyLoadingListenerEvents;
|
||||
|
||||
constructor(options) {
|
||||
this.listeners = {};
|
||||
this.options = options;
|
||||
@@ -454,14 +519,36 @@ class StudyLoadingListener {
|
||||
|
||||
_getSchema(stack) {
|
||||
const imageId = stack.imageIds[0];
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
const colonIndex = imageId.indexOf(':');
|
||||
return imageId.substring(0, colonIndex);
|
||||
}
|
||||
|
||||
// Singleton
|
||||
static getInstance(options) {
|
||||
/**
|
||||
* TODO: Use a different alternative without the use of events.
|
||||
*/
|
||||
const DEFAULT_OPTIONS = {
|
||||
_setProgressData: (progressId, progressData) => {
|
||||
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
|
||||
detail: { progressId, progressData },
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
_clearProgressById: progressId => {
|
||||
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
|
||||
detail: { progressId, percentComplete: 0 },
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
};
|
||||
|
||||
if (!StudyLoadingListener._instance) {
|
||||
StudyLoadingListener._instance = new StudyLoadingListener(options);
|
||||
StudyLoadingListener._instance = new StudyLoadingListener(
|
||||
options || DEFAULT_OPTIONS
|
||||
);
|
||||
}
|
||||
|
||||
return StudyLoadingListener._instance;
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import log from '../log.js';
|
||||
import OHIFError from './OHIFError';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import getImageId from '../utils/getImageId.js';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export class StudyPrefetcher {
|
||||
constructor(studies) {
|
||||
options = {
|
||||
order: 'closest',
|
||||
displaySetCount: 1,
|
||||
onImageCached: noop,
|
||||
requestType: 'prefetch',
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
maxNumPrefetchRequests: 100,
|
||||
includeActiveDisplaySet: false,
|
||||
};
|
||||
|
||||
constructor(studies, options) {
|
||||
this.studies = studies || [];
|
||||
this.prefetchDisplaySetsTimeout = 300;
|
||||
this.lastActiveViewportElement = null;
|
||||
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
this.options.requestType = 'prefetch';
|
||||
}
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
'cornerstoneimagecachefull.StudyPrefetcher',
|
||||
@@ -16,6 +29,9 @@ export class StudyPrefetcher {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove previously set event listeners and stop prefetching.
|
||||
*/
|
||||
destroy() {
|
||||
this.stopPrefetching();
|
||||
cornerstone.events.removeEventListener(
|
||||
@@ -24,95 +40,180 @@ export class StudyPrefetcher {
|
||||
);
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
/**
|
||||
* Get StudyPrefetcher singleton instance.
|
||||
*
|
||||
* @param {array} studies
|
||||
* @param {object} options
|
||||
* @returns
|
||||
*/
|
||||
static getInstance(studies = [], options) {
|
||||
if (!StudyPrefetcher.instance) {
|
||||
StudyPrefetcher.instance = new StudyPrefetcher([]);
|
||||
StudyPrefetcher.instance = new StudyPrefetcher(studies, options);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
this.options.requestType = 'prefetch';
|
||||
}
|
||||
|
||||
return StudyPrefetcher.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* OHIF study metadata instances.
|
||||
*
|
||||
* @param {array} studies
|
||||
*/
|
||||
setStudies(studies) {
|
||||
this.stopPrefetching();
|
||||
this.studies = studies;
|
||||
}
|
||||
|
||||
prefetch() {
|
||||
/**
|
||||
* Get previously prefetched element.
|
||||
*
|
||||
* @returns {HTMLElement} Previously prefetched element.
|
||||
*/
|
||||
getElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch related display sets based on cornerstone viewport element
|
||||
* with previously set options.
|
||||
*
|
||||
* @param {*} element
|
||||
* @param {string} displaySetInstanceUID the display set instance uid
|
||||
* @returns
|
||||
*/
|
||||
prefetch(element, displaySetInstanceUID) {
|
||||
if (!this.studies || !this.studies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
try {
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch {
|
||||
throw new Error('Failed to find the enabled element');
|
||||
}
|
||||
|
||||
this.stopPrefetching();
|
||||
this.prefetchDisplaySets();
|
||||
this.prefetchDisplaySets(displaySetInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop prefetching images.
|
||||
*/
|
||||
stopPrefetching() {
|
||||
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
|
||||
cornerstone.imageLoadPoolManager.clearRequestStack('prefetch');
|
||||
}
|
||||
|
||||
prefetchDisplaySetsAsync(timeout) {
|
||||
timeout = timeout || this.prefetchDisplaySetsTimeout;
|
||||
|
||||
/**
|
||||
* Prefetch display sets async.
|
||||
*
|
||||
* @param {HTMLElement} element cornerstone viewport element
|
||||
* @param {number} timeout
|
||||
*/
|
||||
prefetchDisplaySetsAsync(element, timeout) {
|
||||
try {
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch {
|
||||
throw new Error('Failed to find the enabled element');
|
||||
}
|
||||
timeout = timeout || this.options.prefetchDisplaySetsTimeout;
|
||||
clearTimeout(this.prefetchDisplaySetsHandler);
|
||||
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
||||
this.prefetchDisplaySets();
|
||||
this.prefetchDisplaySets(element);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
prefetchDisplaySets() {
|
||||
// TODO: Allow passing in config
|
||||
let config = {
|
||||
order: 'closest',
|
||||
displaySetCount: 1,
|
||||
};
|
||||
|
||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
|
||||
/**
|
||||
* Extract all image ids from all display sets to be fetched and
|
||||
* call method to add images to request pool manager.
|
||||
*
|
||||
* @param {string} displaySetInstanceUID the display set instance uid
|
||||
*/
|
||||
prefetchDisplaySets(displaySetInstanceUID) {
|
||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
|
||||
|
||||
this.prefetchImageIds(imageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add image ids to request pool manager.
|
||||
*
|
||||
* @param {array} imageIds
|
||||
*/
|
||||
prefetchImageIds(imageIds) {
|
||||
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
||||
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
||||
const requestType = 'prefetch';
|
||||
const preventCache = false;
|
||||
const noop = () => {};
|
||||
const imageLoadPoolManager = cornerstone.imageLoadPoolManager;
|
||||
|
||||
imageLoadPoolManager.maxNumRequests = {
|
||||
...imageLoadPoolManager.maxNumRequests,
|
||||
prefetch: this.options.maxNumPrefetchRequests,
|
||||
};
|
||||
|
||||
let requestFn;
|
||||
if (this.options.preventCache) {
|
||||
requestFn = id => cornerstone.loadImage(id);
|
||||
} else {
|
||||
requestFn = id => cornerstone.loadAndCacheImage(id);
|
||||
}
|
||||
|
||||
nonCachedImageIds.forEach(imageId => {
|
||||
requestPoolManager.addRequest(
|
||||
{},
|
||||
imageId,
|
||||
requestType,
|
||||
preventCache,
|
||||
noop,
|
||||
noop
|
||||
imageLoadPoolManager.addRequest(
|
||||
requestFn.bind(this, imageId),
|
||||
this.options.requestType,
|
||||
{
|
||||
imageId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
requestPoolManager.startGrabbing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get study by cornerstone image instance.
|
||||
*
|
||||
* @param {object} image
|
||||
* @returns
|
||||
*/
|
||||
getStudy(image) {
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
return OHIF.viewer.Studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
const studies = this.studies;
|
||||
return studies.find(
|
||||
study => study.getData().StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get study series by cornerstone image instance.
|
||||
*
|
||||
* @param {object} study OHIF study instance
|
||||
* @param {object} image cornerstone image instance object
|
||||
* @returns
|
||||
*/
|
||||
getSeries(study, image) {
|
||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
||||
'SeriesInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
|
||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
||||
return study.getSeriesByUID(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sop instance by cornerstone image instance.
|
||||
*
|
||||
* @param {array} series
|
||||
* @param {object} image
|
||||
* @returns
|
||||
*/
|
||||
getInstance(series, image) {
|
||||
const instanceMetadata = cornerstone.metaData.get(
|
||||
'instance',
|
||||
@@ -121,7 +222,33 @@ export class StudyPrefetcher {
|
||||
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
||||
}
|
||||
|
||||
getActiveDisplaySet(displaySets, instance) {
|
||||
/**
|
||||
* Returns the display set with given uid.
|
||||
*
|
||||
* @param {string} displaySetInstanceUID the display set instance uid
|
||||
* @returns {object} displaySet
|
||||
*/
|
||||
getDisplaySetByUID(displaySetInstanceUID) {
|
||||
let displaySet;
|
||||
this.studies.forEach(study => {
|
||||
const ds = study.displaySets.find(
|
||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
if (ds) {
|
||||
displaySet = ds;
|
||||
}
|
||||
});
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display set by SOPInstanceUID.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} instance
|
||||
* @returns
|
||||
*/
|
||||
getDisplaySetBySOPInstanceUID(displaySets, instance) {
|
||||
return displaySets.find(displaySet => {
|
||||
return displaySet.images.some(displaySetImage => {
|
||||
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
||||
@@ -129,25 +256,48 @@ export class StudyPrefetcher {
|
||||
});
|
||||
}
|
||||
|
||||
getDisplaySetsToPrefetch(config) {
|
||||
/**
|
||||
* Get active viewport image based on cornerstone viewport element.
|
||||
* @returns
|
||||
*/
|
||||
getActiveViewportImage() {
|
||||
if (!this.enabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.enabledElement.image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch display sets based on cornerstone viewport element image.
|
||||
*
|
||||
* @param {string} displaySetInstanceUID the display set instance uid
|
||||
* @returns {array} displaySets
|
||||
*/
|
||||
getDisplaySetsToPrefetch(displaySetInstanceUID) {
|
||||
const image = this.getActiveViewportImage();
|
||||
|
||||
if (!image || !config || !config.displaySetCount) {
|
||||
if (!image) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/*const study = this.getStudy(image);
|
||||
const study = this.getStudy(image);
|
||||
const series = this.getSeries(study, image);
|
||||
const instance = this.getInstance(series, image);*/
|
||||
const instance = this.getInstance(series, image);
|
||||
const displaySets = study.displaySets;
|
||||
const activeDisplaySet = null; //this.getActiveDisplaySet(displaySets, instance);
|
||||
const activeDisplaySet = displaySetInstanceUID
|
||||
? this.getDisplaySetByUID(displaySetInstanceUID)
|
||||
: this.getDisplaySetBySOPInstanceUID(displaySets, instance);
|
||||
|
||||
const prefetchMethodMap = {
|
||||
topdown: 'getFirstDisplaySets',
|
||||
downward: 'getNextDisplaySets',
|
||||
upward: 'getPreviousDisplaySets',
|
||||
closest: 'getClosestDisplaySets',
|
||||
all: 'getAllDisplaySets',
|
||||
};
|
||||
|
||||
const prefetchOrder = config.order;
|
||||
const prefetchOrder = this.options.order;
|
||||
const methodName = prefetchMethodMap[prefetchOrder];
|
||||
const getDisplaySets = this[methodName];
|
||||
|
||||
@@ -163,18 +313,59 @@ export class StudyPrefetcher {
|
||||
this,
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
config.displaySetCount
|
||||
this.options.displaySetCount,
|
||||
this.options.includeActiveDisplaySet
|
||||
);
|
||||
}
|
||||
|
||||
getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
||||
/**
|
||||
* Get all display sets.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} activeDisplaySet
|
||||
* @param {number} displaySetCount
|
||||
* @param {boolean} includeActiveDisplaySet
|
||||
* @returns
|
||||
*/
|
||||
getAllDisplaySets(
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
displaySetCount,
|
||||
includeActiveDisplaySet
|
||||
) {
|
||||
const length = displaySets.length;
|
||||
const selectedDisplaySets = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const displaySet = displaySets[i];
|
||||
selectedDisplaySets.push(displaySet);
|
||||
}
|
||||
|
||||
return selectedDisplaySets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all display sets in order after the active display set.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} activeDisplaySet
|
||||
* @param {number} displaySetCount
|
||||
* @param {boolean} includeActiveDisplaySet
|
||||
* @returns
|
||||
*/
|
||||
getFirstDisplaySets(
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
displaySetCount,
|
||||
includeActiveDisplaySet
|
||||
) {
|
||||
const length = displaySets.length;
|
||||
const selectedDisplaySets = [];
|
||||
|
||||
for (let i = 0; i < length && displaySetCount; i++) {
|
||||
const displaySet = displaySets[i];
|
||||
|
||||
if (displaySet !== activeDisplaySet) {
|
||||
if (includeActiveDisplaySet || displaySet !== activeDisplaySet) {
|
||||
selectedDisplaySets.push(displaySet);
|
||||
displaySetCount--;
|
||||
}
|
||||
@@ -183,21 +374,78 @@ export class StudyPrefetcher {
|
||||
return selectedDisplaySets;
|
||||
}
|
||||
|
||||
getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
||||
/**
|
||||
* Get all display sets before the active display set.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} activeDisplaySet
|
||||
* @param {number} displaySetCount
|
||||
* @param {boolean} includeActiveDisplaySet
|
||||
* @returns
|
||||
*/
|
||||
getPreviousDisplaySets(
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
displaySetCount,
|
||||
includeActiveDisplaySet
|
||||
) {
|
||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||
const begin = activeDisplaySetIndex + 1;
|
||||
const end = Math.min(begin + displaySetCount, displaySets.length);
|
||||
const end = includeActiveDisplaySet
|
||||
? activeDisplaySetIndex + 1
|
||||
: activeDisplaySetIndex;
|
||||
const previousDisplaySets = displaySets.slice(0, end);
|
||||
return previousDisplaySets.reverse().slice(0, displaySetCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all display sets after the active display set.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} activeDisplaySet
|
||||
* @param {number} displaySetCount
|
||||
* @param {boolean} includeActiveDisplaySet
|
||||
* @returns
|
||||
*/
|
||||
getNextDisplaySets(
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
displaySetCount,
|
||||
includeActiveDisplaySet
|
||||
) {
|
||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||
const begin = includeActiveDisplaySet
|
||||
? activeDisplaySetIndex
|
||||
: activeDisplaySetIndex + 1;
|
||||
const end = Math.min(begin + displaySetCount, displaySets.length);
|
||||
return displaySets.slice(begin, end);
|
||||
}
|
||||
|
||||
getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
||||
/**
|
||||
* Get all display set closest to the active display set.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @param {object} activeDisplaySet
|
||||
* @param {number} displaySetCount
|
||||
* @param {boolean} includeActiveDisplaySet
|
||||
* @returns
|
||||
*/
|
||||
getClosestDisplaySets(
|
||||
displaySets,
|
||||
activeDisplaySet,
|
||||
displaySetCount,
|
||||
includeActiveDisplaySet
|
||||
) {
|
||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||
const length = displaySets.length;
|
||||
const selectedDisplaySets = [];
|
||||
let left = activeDisplaySetIndex - 1;
|
||||
let right = activeDisplaySetIndex + 1;
|
||||
|
||||
if (includeActiveDisplaySet) {
|
||||
selectedDisplaySets.push(displaySets[activeDisplaySetIndex]);
|
||||
displaySetCount--;
|
||||
}
|
||||
|
||||
while ((left >= 0 || right < length) && displaySetCount) {
|
||||
if (left >= 0) {
|
||||
selectedDisplaySets.push(displaySets[left]);
|
||||
@@ -215,6 +463,12 @@ export class StudyPrefetcher {
|
||||
return selectedDisplaySets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all image ids from display sets.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @returns {array} image ids
|
||||
*/
|
||||
getImageIdsFromDisplaySets(displaySets) {
|
||||
let imageIds = [];
|
||||
|
||||
@@ -225,9 +479,19 @@ export class StudyPrefetcher {
|
||||
return imageIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all image ids from a given display set.
|
||||
*
|
||||
* @param {array} displaySet
|
||||
* @returns
|
||||
*/
|
||||
getImageIdsFromDisplaySet(displaySet) {
|
||||
const imageIds = [];
|
||||
|
||||
if (!displaySet.images || displaySet.images.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: This duplicates work done by the stack manager
|
||||
displaySet.images.forEach(image => {
|
||||
const numFrames = image.numFrames;
|
||||
@@ -245,15 +509,30 @@ export class StudyPrefetcher {
|
||||
return imageIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter cached image ids from a set of image ids.
|
||||
*
|
||||
* @param {array} imageIds
|
||||
* @returns {array} images not cached
|
||||
*/
|
||||
filterCachedImageIds(imageIds) {
|
||||
return imageIds.filter(imageId => !this.isImageCached(imageId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if image id is cached in cornerstone.
|
||||
*
|
||||
* @param {string} imageId
|
||||
* @returns
|
||||
*/
|
||||
isImageCached(imageId) {
|
||||
const image = cornerstone.imageCache.imageCache[imageId];
|
||||
return image && image.sizeInBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warns that cache is full and stops prefetching.
|
||||
*/
|
||||
cacheFullHandler = () => {
|
||||
log.warn('Cache full');
|
||||
this.stopPrefetching();
|
||||
|
||||
@@ -4,6 +4,8 @@ import CommandsManager from './CommandsManager.js';
|
||||
import { DICOMFileLoadingListener } from './StudyLoadingListener';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import ImageSet from './ImageSet';
|
||||
import LogManager from './LogManager';
|
||||
import PubSub from './PubSub';
|
||||
import MetadataProvider from './MetadataProvider';
|
||||
import OHIFError from './OHIFError.js';
|
||||
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
|
||||
@@ -36,7 +38,9 @@ const classes = {
|
||||
MetadataProvider,
|
||||
CommandsManager,
|
||||
HotkeysManager,
|
||||
LogManager,
|
||||
ImageSet,
|
||||
PubSub,
|
||||
StudyPrefetcher,
|
||||
StudyLoadingListener,
|
||||
StackLoadingListener,
|
||||
|
||||
@@ -9,9 +9,13 @@ import { SeriesMetadata } from './SeriesMetadata';
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
|
||||
import {
|
||||
isDisplaySetReconstructable,
|
||||
isSpacingUniform,
|
||||
} from '../../utils/isDisplaySetReconstructable';
|
||||
import errorHandler from '../../errorHandler';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook';
|
||||
|
||||
class StudyMetadata extends Metadata {
|
||||
constructor(data, uid) {
|
||||
@@ -158,41 +162,64 @@ class StudyMetadata extends Metadata {
|
||||
// series into another display set.
|
||||
const stackableInstances = [];
|
||||
series.forEachInstance(instance => {
|
||||
let displaySet;
|
||||
|
||||
// All imaging modalities must have a valid value for SOPClassUID (x00080016) or Rows (x00280010)
|
||||
if (
|
||||
!isImage(instance.getTagValue('SOPClassUID')) &&
|
||||
!instance.getTagValue('Rows')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
|
||||
// we set an empty display and we add a isModalitySupported variable to
|
||||
// print a warning that the modality is not supported in the thumbnail.
|
||||
const displaySet = new ImageSet([]);
|
||||
const seriesData = series.getData();
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
isClip: true,
|
||||
displaySetInstanceUID: displaySet.uid,
|
||||
SeriesDate: seriesData.SeriesDate,
|
||||
SeriesTime: seriesData.SeriesTime,
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else if (isSingleImageModality(instance.Modality)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
SeriesNumber: instance.getTagValue('SeriesNumber'),
|
||||
SeriesDescription: instance.getTagValue('SeriesDescription'),
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'),
|
||||
frameRate: instance.getTagValue('FrameTime'),
|
||||
Modality: instance.getTagValue('Modality'),
|
||||
isMultiFrame: false,
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
isReconstructable: false,
|
||||
isModalitySupported: false,
|
||||
metadata: instance.getData().metadata,
|
||||
});
|
||||
|
||||
displaySets.push(displaySet);
|
||||
} else {
|
||||
stackableInstances.push(instance);
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
isClip: true,
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else if (isSingleImageModality(instance.Modality)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else {
|
||||
stackableInstances.push(instance);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,7 +253,6 @@ class StudyMetadata extends Metadata {
|
||||
displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the source display set of the derivated display set.
|
||||
* @param {object} derivatedDisplaySet
|
||||
@@ -241,40 +267,45 @@ class StudyMetadata extends Metadata {
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
|
||||
ds =>
|
||||
ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { metadata } = derivatedDisplaySet;
|
||||
|
||||
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
|
||||
metadata,
|
||||
otherDisplaySets
|
||||
);
|
||||
|
||||
let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
let noReferencedSeriesAvailable =
|
||||
!referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence
|
||||
(metadata);
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
|
||||
metadata
|
||||
);
|
||||
}
|
||||
|
||||
noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
noReferencedSeriesAvailable =
|
||||
!referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
|
||||
metadata,
|
||||
otherDisplaySets
|
||||
);
|
||||
}
|
||||
|
||||
const referencedSeriesAvailable = referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0;
|
||||
const referencedSeriesAvailable =
|
||||
referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0;
|
||||
if (referencedSeriesAvailable) {
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
;
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of derived datasets in the study, filtered by the given filter.
|
||||
@@ -302,7 +333,10 @@ class StudyMetadata extends Metadata {
|
||||
if (referencedSeriesInstanceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet => {
|
||||
return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID;
|
||||
return (
|
||||
StudyMetadata.getReferencedDisplaySet(displaySet, [this])
|
||||
.SeriesInstanceUID === referencedSeriesInstanceUID
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -310,8 +344,7 @@ class StudyMetadata extends Metadata {
|
||||
if (referencedFrameOfReferenceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet =>
|
||||
displaySet.FrameOfReferenceUID ===
|
||||
referencedFrameOfReferenceUID
|
||||
displaySet.FrameOfReferenceUID === referencedFrameOfReferenceUID
|
||||
);
|
||||
}
|
||||
|
||||
@@ -496,6 +529,14 @@ class StudyMetadata extends Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this._displaySets.some(
|
||||
ds => ds.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._displaySets.splice(insertIndex, 0, displaySet);
|
||||
this.displaySets = this._displaySets;
|
||||
}
|
||||
@@ -860,8 +901,9 @@ const makeDisplaySet = (series, instances) => {
|
||||
imageSet.sortByImagePositionPatient();
|
||||
|
||||
// check if the spacing is uniform and update isReconstructable
|
||||
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find
|
||||
(issue => issue === ReconstructionIssues.DATASET_4D);
|
||||
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find(
|
||||
issue => issue === ReconstructionIssues.DATASET_4D
|
||||
);
|
||||
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
|
||||
imageSet.isReconstructable = displaySpacingInfo.isUniform;
|
||||
|
||||
@@ -874,11 +916,15 @@ const makeDisplaySet = (series, instances) => {
|
||||
|
||||
if (!imageSet.displayReconstructableInfo) {
|
||||
// It is not reconstrabale Save type of warning
|
||||
imageSet.reconstructionIssues = displaySpacingInfo ?
|
||||
displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) :
|
||||
displayReconstructableInfo.reconstructionIssues;
|
||||
imageSet.reconstructionIssues = displaySpacingInfo
|
||||
? displayReconstructableInfo.reconstructionIssues.concat(
|
||||
displaySpacingInfo.reconstructionIssues
|
||||
)
|
||||
: displayReconstructableInfo.reconstructionIssues;
|
||||
}
|
||||
|
||||
imageSet.isModalitySupported = true;
|
||||
|
||||
return imageSet;
|
||||
};
|
||||
|
||||
@@ -939,6 +985,7 @@ function _getDisplaySetFromSopClassModule(
|
||||
url: study.getData().wadoRoot,
|
||||
headers,
|
||||
errorInterceptor,
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
let displaySet = plugin.getDisplaySetFromSeries(
|
||||
@@ -955,38 +1002,36 @@ function _getDisplaySetFromSopClassModule(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedSeriesSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence (
|
||||
metadata,
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedSeriesSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
|
||||
metadata
|
||||
) {
|
||||
if (!metadata.ReferencedSeriesSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
const ReferencedSeriesSequence = _toArray(
|
||||
metadata.ReferencedSeriesSequence
|
||||
);
|
||||
const ReferencedSeriesSequence = _toArray(metadata.ReferencedSeriesSequence);
|
||||
|
||||
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
@@ -1007,22 +1052,25 @@ function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
if (
|
||||
referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SourceImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SourceImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
@@ -1032,9 +1080,7 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
SourceImageSequence = metadata.SourceImageSequence;
|
||||
} else {
|
||||
const { PerFrameFunctionalGroupsSequence } = metadata;
|
||||
const firstFunctionalGroups = _toArray(
|
||||
PerFrameFunctionalGroupsSequence
|
||||
)[0];
|
||||
const firstFunctionalGroups = _toArray(PerFrameFunctionalGroupsSequence)[0];
|
||||
if (firstFunctionalGroups) {
|
||||
const { DerivationImageSequence } = firstFunctionalGroups;
|
||||
SourceImageSequence = DerivationImageSequence;
|
||||
@@ -1054,22 +1100,25 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
displaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
if (
|
||||
referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SOPInstanceUID of the displaySets.
|
||||
* @param {array[object]} displaysets
|
||||
* @param {string} SOPInstanceUID
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SOPInstanceUID of the displaySets.
|
||||
* @param {array[object]} displaysets
|
||||
* @param {string} SOPInstanceUID
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
displaySets,
|
||||
SOPInstanceUID
|
||||
) {
|
||||
@@ -1090,10 +1139,10 @@ function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _toArray(arrayOrObject) {
|
||||
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
|
||||
}
|
||||
|
||||
export {StudyMetadata};
|
||||
export { StudyMetadata };
|
||||
@@ -78,17 +78,6 @@ export const setActiveViewportSpecificData = viewportSpecificData => ({
|
||||
/**
|
||||
* NOT-VIEWPORT
|
||||
*/
|
||||
export const setStudyLoadingProgress = (progressId, progressData) => ({
|
||||
type: 'SET_STUDY_LOADING_PROGRESS',
|
||||
progressId,
|
||||
progressData,
|
||||
});
|
||||
|
||||
export const clearStudyLoadingProgress = progressId => ({
|
||||
type: 'CLEAR_STUDY_LOADING_PROGRESS',
|
||||
progressId,
|
||||
});
|
||||
|
||||
export const setUserPreferences = state => ({
|
||||
type: SET_USER_PREFERENCES,
|
||||
state,
|
||||
@@ -134,8 +123,6 @@ const actions = {
|
||||
/**
|
||||
* NOT-VIEWPORT
|
||||
*/
|
||||
setStudyLoadingProgress,
|
||||
clearStudyLoadingProgress,
|
||||
setUserPreferences,
|
||||
setExtensionData,
|
||||
setTimepoints,
|
||||
|
||||
@@ -11,8 +11,6 @@ describe('actions', () => {
|
||||
'setLayout',
|
||||
'clearViewportSpecificData',
|
||||
'setActiveViewportSpecificData',
|
||||
'setStudyLoadingProgress',
|
||||
'clearStudyLoadingProgress',
|
||||
'setUserPreferences',
|
||||
'setExtensionData',
|
||||
'setTimepoints',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import extensions from './extensions';
|
||||
import loading from './loading';
|
||||
import preferences from './preferences';
|
||||
import servers from './servers';
|
||||
import studies from './studies';
|
||||
@@ -8,7 +7,6 @@ import viewports from './viewports';
|
||||
|
||||
const reducers = {
|
||||
extensions,
|
||||
loading,
|
||||
preferences,
|
||||
servers,
|
||||
studies,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const defaultState = {
|
||||
progress: {},
|
||||
lastUpdated: null,
|
||||
};
|
||||
|
||||
const loading = (state = defaultState, action) => {
|
||||
let progress;
|
||||
let lastUpdated;
|
||||
switch (action.type) {
|
||||
case 'SET_STUDY_LOADING_PROGRESS':
|
||||
progress = cloneDeep(state).progress;
|
||||
progress[action.progressId] = action.progressData;
|
||||
|
||||
// This is a workaround so we can easily identify changes
|
||||
// to the progress object without doing deep comparison.
|
||||
lastUpdated = new Date().getTime();
|
||||
|
||||
return Object.assign({}, state, { progress, lastUpdated });
|
||||
case 'CLEAR_STUDY_LOADING_PROGRESS':
|
||||
progress = cloneDeep(state).progress;
|
||||
delete progress[action.progressId];
|
||||
|
||||
lastUpdated = new Date().getTime();
|
||||
|
||||
return Object.assign({}, state, { progress, lastUpdated });
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default loading;
|
||||
@@ -260,7 +260,6 @@ class MeasurementService {
|
||||
const sourceInfo = this._getSourceInfo(source);
|
||||
|
||||
if (!definition) {
|
||||
console.log('TEST');
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
/**
|
||||
* An implementation of the static wado client, that fetches data from
|
||||
* a static response rather than actually doing real queries. This allows
|
||||
* fast encoding of test data, but because it is static, anything actually
|
||||
* performing searches doesn't work. This version fixes the query issue
|
||||
* by manually implementing a query option.
|
||||
*/
|
||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
static filterKeys = {
|
||||
StudyInstanceUID: '0020000D',
|
||||
PatientName: '00100010',
|
||||
PatientID: '00100020',
|
||||
StudyDescription: '00081030',
|
||||
ModalitiesInStudy: '00080061',
|
||||
};
|
||||
|
||||
constructor(qidoConfig) {
|
||||
super(qidoConfig);
|
||||
this.staticWado = qidoConfig.staticWado;
|
||||
this.extendMetadataWithInstances = qidoConfig.extendMetadataWithInstances;
|
||||
}
|
||||
|
||||
async retrieveSeriesMetadata(options) {
|
||||
if (!this.extendMetadataWithInstances)
|
||||
return super.retrieveSeriesMetadata(options);
|
||||
const results = await Promise.all([
|
||||
super.retrieveSeriesMetadata(options),
|
||||
this.searchForInstances(options),
|
||||
]);
|
||||
const metadata = results[0];
|
||||
const instances = results[1];
|
||||
return metadata.map(item => {
|
||||
const sopUID = item['00080018'].Value[0];
|
||||
const instance = instances.find(
|
||||
instance => instance['00080018'].Value[0] == sopUID
|
||||
);
|
||||
Object.assign(item, instance);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
async searchForStudies(options) {
|
||||
if (!this.staticWado) return super.searchForStudies(options);
|
||||
|
||||
let searchResult = await super.searchForStudies(options);
|
||||
const { queryParams } = options;
|
||||
if (!queryParams) return searchResult;
|
||||
const filtered = searchResult.filter(study => {
|
||||
for (const key of Object.keys(StaticWadoClient.filterKeys)) {
|
||||
if (!this.filterItem(key, queryParams, study)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
filterItem(key, queryParams, study) {
|
||||
const altKey = StaticWadoClient.filterKeys[key] || key;
|
||||
if (!queryParams) return true;
|
||||
const testValue = queryParams[key] || queryParams[altKey];
|
||||
if (!testValue) return true;
|
||||
const valueElem = study[key] || study[altKey];
|
||||
if (!valueElem) return false;
|
||||
const value = valueElem.Value;
|
||||
return this.eqItem(testValue, value);
|
||||
}
|
||||
|
||||
eqItem(testValue, value) {
|
||||
if (testValue.filter) {
|
||||
return (
|
||||
testValue.filter(testItem => this.eqItem(testItem, value)).length > 0
|
||||
);
|
||||
}
|
||||
if (value === testValue) return true;
|
||||
if (typeof value == 'string') {
|
||||
return value.indexOf(testValue) != -1;
|
||||
}
|
||||
if (value.Alphabetic) return this.eqItem(testValue, value.Alphabetic);
|
||||
if (value.filter)
|
||||
return value.filter(item => this.eqItem(testValue, item)).length > 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import StaticWadoClient from './StaticWadoClient';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
/**
|
||||
* Creates a QIDO date string for a date range query
|
||||
@@ -114,13 +116,18 @@ function resultDataToStudies(resultData) {
|
||||
}
|
||||
|
||||
export default function Studies(server, filter) {
|
||||
const { staticWado } = server;
|
||||
const config = {
|
||||
...server,
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const dicomWeb = staticWado
|
||||
? new StaticWadoClient(config)
|
||||
: new api.DICOMwebClient(config);
|
||||
server.qidoSupportsIncludeField =
|
||||
server.qidoSupportsIncludeField === undefined
|
||||
? true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import StaticWadoClient from '../qido/StaticWadoClient';
|
||||
import dcmjs from 'dcmjs';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from './studyInstanceHelpers';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import { getXHRRetryRequestHook } from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
|
||||
@@ -72,10 +73,12 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
configLoad() {
|
||||
const { server } = this;
|
||||
|
||||
const client = new api.DICOMwebClient({
|
||||
const client = new StaticWadoClient({
|
||||
...server,
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
/**
|
||||
* Class for sync load of study metadata.
|
||||
@@ -61,6 +62,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
|
||||
url: server.wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
@@ -116,6 +116,11 @@ async function makeSOPInstance(server, study, instance) {
|
||||
};
|
||||
study.seriesMap[SeriesInstanceUID] = series;
|
||||
study.series.push(series);
|
||||
} else {
|
||||
if (series.SeriesDate === undefined)
|
||||
series.SeriesDate = naturalizedInstance.SeriesDate;
|
||||
if (series.SeriesTime === undefined)
|
||||
series.SeriesTime = naturalizedInstance.SeriesTime;
|
||||
}
|
||||
|
||||
const wadouri = buildInstanceWadoUrl(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
|
||||
import errorHandler from '../errorHandler';
|
||||
import getXHRRetryRequestHook from './xhrRetryRequestHook';
|
||||
|
||||
const getImageId = imageObj => {
|
||||
if (!imageObj) {
|
||||
@@ -66,6 +67,7 @@ const wadorsRetriever = (
|
||||
url,
|
||||
headers,
|
||||
errorInterceptor,
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import isDicomUid from './isDicomUid';
|
||||
import resolveObjectPath from './resolveObjectPath';
|
||||
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||
import xhrRetryRequestHook from './xhrRetryRequestHook';
|
||||
|
||||
const utils = {
|
||||
guid,
|
||||
@@ -40,6 +41,7 @@ const utils = {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
xhrRetryRequestHook,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -63,6 +65,7 @@ export {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
xhrRetryRequestHook,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -23,6 +23,7 @@ describe('Top level exports', () => {
|
||||
'resolveObjectPath',
|
||||
'hierarchicalListUtils',
|
||||
'progressTrackingUtils',
|
||||
'xhrRetryRequestHook',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(utils.default).sort();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReconstructionIssues } from './../enums.js';
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
@@ -78,7 +77,9 @@ function processSingleframe(instances) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
|
||||
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
|
||||
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
|
||||
} else if (
|
||||
!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
|
||||
}
|
||||
|
||||
@@ -92,7 +93,10 @@ function processSingleframe(instances) {
|
||||
reconstructionIssues.push(ReconstructionIssues.DATASET_4D);
|
||||
}
|
||||
|
||||
return { value: reconstructionIssues.length === 0 ? true : false, reconstructionIssues };
|
||||
return {
|
||||
value: reconstructionIssues.length === 0 ? true : false,
|
||||
reconstructionIssues,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,14 +120,12 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (n > 2) {
|
||||
const lastIpp = instances[n - 1].getData().metadata
|
||||
.ImagePositionPatient;
|
||||
const lastIpp = instances[n - 1].getData().metadata.ImagePositionPatient;
|
||||
|
||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||
if (firstImagePositionPatient && lastIpp) {
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(n - 1);
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (n - 1);
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
@@ -136,7 +138,7 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
previousImagePositionPatient
|
||||
);
|
||||
|
||||
if (datasetIs4D && spacingBetweenFrames < 1.e-3) {
|
||||
if (datasetIs4D && spacingBetweenFrames < 1e-3) {
|
||||
// the dataset is 4D, if the distance is zero, means that we are
|
||||
// checking the 4th dimension. Do not return, since we want still to
|
||||
// check the 3rd dimension spacing.
|
||||
@@ -164,10 +166,13 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
}
|
||||
}
|
||||
|
||||
return { isUniform: reconstructionIssues.length === 0 ? true : false, missingFrames, reconstructionIssues };
|
||||
return {
|
||||
isUniform: reconstructionIssues.length === 0 ? true : false,
|
||||
missingFrames,
|
||||
reconstructionIssues,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if 4D dataset.
|
||||
*
|
||||
@@ -181,7 +186,7 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
*
|
||||
* @returns {boolean} dataset4D value.
|
||||
*/
|
||||
function _isDataset4D(instances) {
|
||||
function _isDataset4D(instances) {
|
||||
const n = instances.length;
|
||||
for (let ii = 0; ii < n; ++ii) {
|
||||
const instanceMetadataControl = instances[ii].getData().metadata;
|
||||
@@ -204,7 +209,12 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) {
|
||||
if (
|
||||
_isSameArray(
|
||||
instanceMetadataControl.ImagePositionPatient,
|
||||
instanceMetadata.ImagePositionPatient
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -265,11 +275,11 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
return Math.sqrt(
|
||||
Math.pow(a[0] - b[0], 2) +
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
);
|
||||
}
|
||||
|
||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||
|
||||
export {isDisplaySetReconstructable, isSpacingUniform};
|
||||
export { isDisplaySetReconstructable, isSpacingUniform };
|
||||
@@ -4,6 +4,7 @@ import str2ab from '../str2ab';
|
||||
import unpackOverlay from './unpackOverlay';
|
||||
|
||||
import errorHandler from '../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
|
||||
|
||||
export default async function fetchOverlayData(instance, server) {
|
||||
const OverlayDataPromises = [];
|
||||
@@ -69,6 +70,7 @@ async function _getOverlayData(tag, server) {
|
||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import DICOMWeb from '../../DICOMWeb';
|
||||
import str2ab from '../str2ab';
|
||||
|
||||
import errorHandler from '../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
|
||||
|
||||
export default async function fetchPaletteColorLookupTableData(
|
||||
instance,
|
||||
@@ -141,6 +142,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import retry from 'retry';
|
||||
|
||||
const defaultRetryOptions = {
|
||||
retries: 5,
|
||||
factor: 3,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: 60 * 1000,
|
||||
randomize: true,
|
||||
retryableStatusCodes: [429, 500],
|
||||
};
|
||||
|
||||
let retryOptions = { ...defaultRetryOptions };
|
||||
|
||||
/**
|
||||
* Request hook used to add retry functionality to XHR requests.
|
||||
*
|
||||
* @param {XMLHttpRequest} request XHR request instance
|
||||
* @param {object} metadata Metadata about the request
|
||||
* @param {object} metadata.url URL
|
||||
* @param {object} metadata.method HTTP method
|
||||
* @returns {XMLHttpRequest} request instance optionally modified
|
||||
*/
|
||||
const xhrRetryRequestHook = (request, metadata) => {
|
||||
const { url, method } = metadata;
|
||||
|
||||
function faultTolerantRequestSend(...args) {
|
||||
const operation = retry.operation(retryOptions);
|
||||
|
||||
operation.attempt(function operationAttempt(currentAttempt) {
|
||||
const originalOnReadyStateChange = request.onreadystatechange;
|
||||
|
||||
/** Overriding/extending XHR function */
|
||||
request.onreadystatechange = function onReadyStateChange(...args) {
|
||||
originalOnReadyStateChange.apply(request, args);
|
||||
|
||||
if (retryOptions.retryableStatusCodes.includes(request.status)) {
|
||||
const errorMessage = `Attempt to request ${url} failed.`;
|
||||
const attemptFailedError = new Error(errorMessage);
|
||||
operation.retry(attemptFailedError);
|
||||
}
|
||||
};
|
||||
|
||||
/** Call open only on retry (after headers and other things were set in the xhr instance) */
|
||||
if (currentAttempt > 1) {
|
||||
console.warn(`Requesting ${url}... (attempt: ${currentAttempt})`);
|
||||
request.open(method, url, true);
|
||||
}
|
||||
});
|
||||
|
||||
originalRequestSend.apply(request, args);
|
||||
}
|
||||
|
||||
/** Overriding/extending XHR function */
|
||||
const originalRequestSend = request.send;
|
||||
request.send = faultTolerantRequestSend;
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a configured retry request hook function
|
||||
* that can be used to add retry functionality to XHR request.
|
||||
*
|
||||
* Default options:
|
||||
* retries: 5
|
||||
* factor: 3
|
||||
* minTimeout: 1 * 1000
|
||||
* maxTimeout: 60 * 1000
|
||||
* randomize: true
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.retires number of retries
|
||||
* @param {number} options.factor factor
|
||||
* @param {number} options.minTimeout the min timeout
|
||||
* @param {number} options.maxTimeout the max timeout
|
||||
* @param {boolean} options.randomize randomize
|
||||
* @param {array} options.retryableStatusCodes status codes that can trigger retry
|
||||
* @returns {function} the configured retry request function
|
||||
*/
|
||||
export const getXHRRetryRequestHook = (options = {}) => {
|
||||
retryOptions = { ...defaultRetryOptions };
|
||||
if ('retries' in options) {
|
||||
retryOptions.retries = options.retries;
|
||||
}
|
||||
if ('factor' in options) {
|
||||
retryOptions.factor = options.factor;
|
||||
}
|
||||
if ('minTimeout' in options) {
|
||||
retryOptions.minTimeout = options.minTimeout;
|
||||
}
|
||||
if ('maxTimeout' in options) {
|
||||
retryOptions.maxTimeout = options.maxTimeout;
|
||||
}
|
||||
if ('randomize' in options) {
|
||||
retryOptions.randomize = options.randomize;
|
||||
}
|
||||
if ('retryableStatusCodes' in options) {
|
||||
retryOptions.retryableStatusCodes = options.retryableStatusCodes;
|
||||
}
|
||||
return xhrRetryRequestHook;
|
||||
};
|
||||
|
||||
export default getXHRRetryRequestHook;
|
||||
@@ -3,6 +3,68 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.4...@ohif/ui@1.10.5) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.3...@ohif/ui@1.10.4) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.2...@ohif/ui@1.10.3) (2021-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.1...@ohif/ui@1.10.2) (2021-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* throttle thumbnail progress updates ([#2557](https://github.com/OHIF/Viewers/issues/2557)) ([66c587b](https://github.com/OHIF/Viewers/commit/66c587b139835201331a64bc24a17901e7583649))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.0...@ohif/ui@1.10.1) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.3...@ohif/ui@1.10.0) (2021-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.2...@ohif/ui@1.9.3) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.1...@ohif/ui@1.9.2) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "1.9.2",
|
||||
"version": "1.10.5",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .badgeNumber-container {
|
||||
background-color: var(--ui-border-color-dark);
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
bottom: 10px;
|
||||
left: 5px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .badgeNumber {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -36,7 +59,12 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .roundedButton svg .roundedButtonWrapper .roundedButton span {
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
svg
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
span {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class RoundedButtonGroup extends Component {
|
||||
PropTypes.shape({
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
stateEvent: PropTypes.string,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
@@ -30,6 +31,15 @@ class RoundedButtonGroup extends Component {
|
||||
value: null,
|
||||
};
|
||||
|
||||
state = {
|
||||
badgeNumbers: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onStateEvent = this.onStateEvent.bind(this);
|
||||
}
|
||||
|
||||
onClickOption = value => {
|
||||
let newValue = value;
|
||||
if (this.props.value === value) {
|
||||
@@ -41,6 +51,48 @@ class RoundedButtonGroup extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onStateEvent(event) {
|
||||
const optionIndex = this.props.options.findIndex(
|
||||
o => o.value === event.detail.target
|
||||
);
|
||||
if (optionIndex > -1) {
|
||||
const badgeNumbers = this.state.badgeNumbers;
|
||||
badgeNumbers[optionIndex] = event.detail.badgeNumber;
|
||||
this.setState({ badgeNumbers });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.options.forEach(option => {
|
||||
if (option.stateEvent) {
|
||||
document.addEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.props.options.forEach((option, index) => {
|
||||
if (
|
||||
option.stateEvent &&
|
||||
option.stateEvent !==
|
||||
(prevProps.options[index]
|
||||
? prevProps.options[index].stateEvent
|
||||
: null)
|
||||
) {
|
||||
document.removeEventListener(option.stateEvent, this.onStateEvent);
|
||||
document.addEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.options.forEach(option => {
|
||||
if (option.stateEvent) {
|
||||
document.removeEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = classnames(
|
||||
RoundedButtonGroup.className,
|
||||
@@ -62,6 +114,14 @@ class RoundedButtonGroup extends Component {
|
||||
<div className="bottomLabel">{option.bottomLabel}</div>
|
||||
);
|
||||
|
||||
let badgeNumber = this.state.badgeNumbers[index];
|
||||
const badgeNumberOverflow = String(badgeNumber).length > 2;
|
||||
badgeNumber = badgeNumber
|
||||
? badgeNumberOverflow
|
||||
? 99
|
||||
: badgeNumber
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -70,6 +130,14 @@ class RoundedButtonGroup extends Component {
|
||||
>
|
||||
<div className="roundedButton">
|
||||
{optionText}
|
||||
{badgeNumber && (
|
||||
<div className="badgeNumber-container">
|
||||
<span className="badgeNumber">
|
||||
{badgeNumber}
|
||||
{badgeNumberOverflow && '+'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{iconProps && <Icon {...iconProps} />}
|
||||
</div>
|
||||
{bottomLabel}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import './ImageThumbnail.styl';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import React, { useState, useEffect, createRef } from 'react';
|
||||
import React, { useState, useEffect, createRef, useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -23,6 +23,7 @@ function ImageThumbnail(props) {
|
||||
imageId,
|
||||
stackPercentComplete,
|
||||
error: propsError,
|
||||
showProgressBar,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
@@ -39,7 +40,8 @@ function ImageThumbnail(props) {
|
||||
loadingOrError = <ViewportLoadingIndicator />;
|
||||
}
|
||||
|
||||
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
|
||||
const showStackLoadingProgressBar =
|
||||
showProgressBar && stackPercentComplete !== undefined;
|
||||
|
||||
const shouldRenderToCanvas = () => {
|
||||
return imageId && !imageSrc;
|
||||
@@ -71,11 +73,11 @@ function ImageThumbnail(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const purgeCancelablePromise = () => {
|
||||
const purgeCancelablePromise = useCallback(() => {
|
||||
if (cancelablePromise) {
|
||||
cancelablePromise.cancel();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -96,7 +98,13 @@ function ImageThumbnail(props) {
|
||||
setImagePromise();
|
||||
fetchImagePromise();
|
||||
}
|
||||
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
|
||||
}, [
|
||||
fetchImagePromise,
|
||||
image.imageId,
|
||||
imageId,
|
||||
purgeCancelablePromise,
|
||||
setImagePromise,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={classNames('ImageThumbnail', { active: active })}>
|
||||
@@ -104,14 +112,14 @@ function ImageThumbnail(props) {
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
@@ -135,6 +143,7 @@ ImageThumbnail.propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
stackPercentComplete: PropTypes.number.isRequired,
|
||||
showProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
ImageThumbnail.defaultProps = {
|
||||
@@ -143,6 +152,7 @@ ImageThumbnail.defaultProps = {
|
||||
stackPercentComplete: 0,
|
||||
width: 217,
|
||||
height: 123,
|
||||
showProgressBar: true,
|
||||
};
|
||||
|
||||
export default ImageThumbnail;
|
||||
@@ -9,6 +9,7 @@ function StudyBrowser(props) {
|
||||
onThumbnailClick,
|
||||
onThumbnailDoubleClick,
|
||||
supportsDrag,
|
||||
showThumbnailProgressBar,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -24,12 +25,12 @@ function StudyBrowser(props) {
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
InstanceNumber,
|
||||
derivedDisplaySetsNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
stackPercentComplete,
|
||||
hasWarnings,
|
||||
hasDerivedDisplaySets,
|
||||
} = thumb;
|
||||
|
||||
return (
|
||||
@@ -48,19 +49,20 @@ function StudyBrowser(props) {
|
||||
// Thumb
|
||||
altImageText={altImageText}
|
||||
imageId={imageId}
|
||||
InstanceNumber={InstanceNumber}
|
||||
derivedDisplaySetsNumber={derivedDisplaySetsNumber}
|
||||
displaySetInstanceUID={displaySetInstanceUID} // used by drop
|
||||
numImageFrames={numImageFrames}
|
||||
SeriesDescription={SeriesDescription}
|
||||
SeriesNumber={SeriesNumber}
|
||||
hasWarnings={hasWarnings}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
hasDerivedDisplaySets={hasDerivedDisplaySets}
|
||||
// Events
|
||||
onClick={onThumbnailClick.bind(
|
||||
undefined,
|
||||
displaySetInstanceUID
|
||||
)}
|
||||
onDoubleClick={onThumbnailDoubleClick}
|
||||
showProgressBar={showThumbnailProgressBar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -83,7 +85,7 @@ StudyBrowser.propTypes = {
|
||||
altImageText: PropTypes.string,
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
imageId: PropTypes.string,
|
||||
InstanceNumber: PropTypes.number,
|
||||
derivedDisplaySetsNumber: PropTypes.number,
|
||||
numImageFrames: PropTypes.number,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.number,
|
||||
@@ -95,6 +97,7 @@ StudyBrowser.propTypes = {
|
||||
supportsDrag: PropTypes.bool,
|
||||
onThumbnailClick: PropTypes.func,
|
||||
onThumbnailDoubleClick: PropTypes.func,
|
||||
showThumbnailProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
StudyBrowser.defaultProps = {
|
||||
@@ -102,6 +105,7 @@ StudyBrowser.defaultProps = {
|
||||
supportsDrag: true,
|
||||
onThumbnailClick: noop,
|
||||
onThumbnailDoubleClick: noop,
|
||||
showThumbnailProgressBar: true,
|
||||
};
|
||||
|
||||
export { StudyBrowser };
|
||||
@@ -1,34 +1,43 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { classes } from '@ohif/core';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { Tooltip } from './../tooltip';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
|
||||
import './Thumbnail.styl';
|
||||
|
||||
const StudyLoadingListener = classes.StudyLoadingListener;
|
||||
|
||||
function ThumbnailFooter({
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
hasWarnings
|
||||
hasWarnings,
|
||||
hasDerivedDisplaySets,
|
||||
}) {
|
||||
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
||||
const [derivedDisplaySetsActive, derivedDisplaySetsActiveSet] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false
|
||||
let unmounted = false;
|
||||
hasWarnings.then(response => {
|
||||
if (!unmounted) {
|
||||
inconsistencyWarningsSet(response)
|
||||
inconsistencyWarningsSet(response);
|
||||
}
|
||||
})
|
||||
});
|
||||
hasDerivedDisplaySets.then(response => {
|
||||
if (!unmounted) {
|
||||
derivedDisplaySetsActiveSet(response);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unmounted = true
|
||||
}
|
||||
}, [])
|
||||
unmounted = true;
|
||||
};
|
||||
}, [hasWarnings, hasDerivedDisplaySets]);
|
||||
|
||||
const infoOnly = !SeriesDescription;
|
||||
|
||||
@@ -41,7 +50,7 @@ function ThumbnailFooter({
|
||||
);
|
||||
};
|
||||
|
||||
const getWarningContent = (inconsistencyWarnings) => {
|
||||
const getWarningContent = inconsistencyWarnings => {
|
||||
if (Array.isArray(inconsistencyWarnings)) {
|
||||
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
@@ -54,8 +63,8 @@ function ThumbnailFooter({
|
||||
};
|
||||
|
||||
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
|
||||
return(
|
||||
<React.Fragment>
|
||||
return (
|
||||
<React.Fragment>
|
||||
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
|
||||
<OverlayTrigger
|
||||
key={SeriesNumber}
|
||||
@@ -67,7 +76,9 @@ function ThumbnailFooter({
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">Series Inconsistencies</div>
|
||||
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
|
||||
<div className="warningContent">
|
||||
{getWarningContent(inconsistencyWarnings)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -79,34 +90,67 @@ function ThumbnailFooter({
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const getDerivedInfo = derivedDisplaySetsActive => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{derivedDisplaySetsActive ? (
|
||||
<div className="derived">
|
||||
<Icon name="link" />
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getSeriesInformation = (
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
inconsistencyWarnings
|
||||
inconsistencyWarnings,
|
||||
derivedDisplaySetsActive
|
||||
) => {
|
||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||
if (!SeriesNumber && !numImageFrames) {
|
||||
return;
|
||||
}
|
||||
const seriesInformation =
|
||||
const seriesInformation = (
|
||||
<div className="series-information">
|
||||
{getInfo(SeriesNumber, 'S:')}
|
||||
{getInfo(InstanceNumber, 'I:')}
|
||||
{getInfo(numImageFrames, '', 'image-frames')}
|
||||
<React.Fragment>
|
||||
{SeriesNumber !== undefined ? (
|
||||
getInfo(SeriesNumber, 'S:')
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
{numImageFrames !== undefined ? (
|
||||
getInfo(numImageFrames, '', 'image-frames')
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
{getDerivedInfo(derivedDisplaySetsActive)}
|
||||
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (seriesInformation);
|
||||
return seriesInformation;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||
<div className="series-description">{SeriesDescription}</div>
|
||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
|
||||
{getSeriesInformation(
|
||||
SeriesNumber,
|
||||
numImageFrames,
|
||||
inconsistencyWarnings,
|
||||
derivedDisplaySetsActive
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -119,19 +163,37 @@ function Thumbnail(props) {
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
imageSrc,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
hasWarnings,
|
||||
stackPercentComplete,
|
||||
StudyInstanceUID,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onMouseDown,
|
||||
supportsDrag,
|
||||
showProgressBar,
|
||||
} = props;
|
||||
|
||||
const [stackPercentComplete, setStackPercentComplete] = useState(0);
|
||||
useEffect(() => {
|
||||
const onProgressChange = throttle(({ detail }) => {
|
||||
const { progressId, progressData } = detail;
|
||||
if (`StackProgress:${displaySetInstanceUID}` === progressId) {
|
||||
const percent = progressData ? progressData.percentComplete : 0;
|
||||
setStackPercentComplete(percent);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
document.addEventListener(
|
||||
StudyLoadingListener.events.OnProgress,
|
||||
onProgressChange
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
StudyLoadingListener.events.OnProgress,
|
||||
onProgressChange
|
||||
);
|
||||
};
|
||||
}, [displaySetInstanceUID]);
|
||||
|
||||
const [collectedProps, drag, dragPreview] = useDrag({
|
||||
// `droppedItem` in `dropTarget`
|
||||
// The only data it will have access to
|
||||
@@ -164,6 +226,7 @@ function Thumbnail(props) {
|
||||
imageId={imageId}
|
||||
error={error}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
showProgressBar={showProgressBar}
|
||||
/>
|
||||
)}
|
||||
{/* SHOW TEXT ALTERNATIVE */}
|
||||
@@ -197,12 +260,13 @@ Thumbnail.propTypes = {
|
||||
altImageText: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hasWarnings: PropTypes.instanceOf(Promise),
|
||||
hasDerivedDisplaySets: PropTypes.instanceOf(Promise),
|
||||
numImageFrames: PropTypes.number,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
showProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
Thumbnail.defaultProps = {
|
||||
|
||||
@@ -6,10 +6,13 @@ import React, {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { classes } from '@ohif/core';
|
||||
|
||||
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
|
||||
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
|
||||
|
||||
const { LogManager } = classes;
|
||||
|
||||
const SnackbarContext = createContext(null);
|
||||
|
||||
export const useSnackbarContext = () => useContext(SnackbarContext);
|
||||
@@ -28,6 +31,20 @@ const SnackbarProvider = ({ children, service }) => {
|
||||
const [count, setCount] = useState(1);
|
||||
const [snackbarItems, setSnackbarItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const onLogHandler = ({ type, notify, title, message }) => {
|
||||
if (notify) {
|
||||
show({ type, title, message });
|
||||
}
|
||||
};
|
||||
|
||||
LogManager.subscribe(LogManager.EVENTS.OnLog, onLogHandler);
|
||||
|
||||
return () => {
|
||||
LogManager.unsubscribe(LogManager.EVENTS.OnLog, onLogHandler);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const show = useCallback(
|
||||
options => {
|
||||
if (!options || (!options.title && !options.message)) {
|
||||
|
||||
@@ -12,8 +12,6 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
// ~~ Rules
|
||||
const extractStyleChunksRule = require('./rules/extractStyleChunks.js');
|
||||
// ~~ Directories
|
||||
@@ -41,6 +39,23 @@ module.exports = (env, argv) => {
|
||||
path: DIST_DIR,
|
||||
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
|
||||
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
|
||||
devtoolModuleFilenameTemplate: function(info) {
|
||||
if (isProdBuild) {
|
||||
return `webpack:///${info.resourcePath}`;
|
||||
} else {
|
||||
return 'file:///' + encodeURI(info.absoluteResourcePath);
|
||||
}
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// We use this alias and the CopyPlugin below to support using the dynamic-import version
|
||||
// of WADO Image Loader, but only when building a PWA. When we build a package, we must use the
|
||||
// bundled version of WADO Image Loader so we can produce a single file for the viewer.
|
||||
// (Note: script-tag version of the viewer will no longer be supported in OHIF v3)
|
||||
alias: {
|
||||
'cornerstone-wado-image-loader':
|
||||
'cornerstone-wado-image-loader/dist/dynamic-import/cornerstoneWADOImageLoader.min.js',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [...extractStyleChunksRule(isProdBuild)],
|
||||
@@ -71,6 +86,11 @@ module.exports = (env, argv) => {
|
||||
from: `${PUBLIC_DIR}/${APP_CONFIG}`,
|
||||
to: `${DIST_DIR}/app-config.js`,
|
||||
},
|
||||
{
|
||||
from:
|
||||
'../../../node_modules/cornerstone-wado-image-loader/dist/dynamic-import',
|
||||
to: DIST_DIR,
|
||||
},
|
||||
]),
|
||||
// https://github.com/faceyspacey/extract-css-chunks-webpack-plugin#webpack-4-standalone-installation
|
||||
new ExtractCssChunksPlugin({
|
||||
@@ -95,6 +115,15 @@ module.exports = (env, argv) => {
|
||||
// maximumFileSizeToCacheInBytes: 4 * 1024 * 1024
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
// include all types of chunks
|
||||
chunks: 'all',
|
||||
},
|
||||
//runtimeChunk: 'single',
|
||||
minimize: isProdBuild,
|
||||
sideEffects: true,
|
||||
},
|
||||
// https://webpack.js.org/configuration/dev-server/
|
||||
devServer: {
|
||||
// gzip compression of everything served
|
||||
|
||||
@@ -3,6 +3,213 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [4.12.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.11...@ohif/viewer@4.12.12) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.10...@ohif/viewer@4.12.11) (2022-01-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.9...@ohif/viewer@4.12.10) (2022-01-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.8...@ohif/viewer@4.12.9) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.7...@ohif/viewer@4.12.8) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.6...@ohif/viewer@4.12.7) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.5...@ohif/viewer@4.12.6) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.4...@ohif/viewer@4.12.5) (2021-10-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.3...@ohif/viewer@4.12.4) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.2...@ohif/viewer@4.12.3) (2021-10-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.1...@ohif/viewer@4.12.2) (2021-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.12.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.0...@ohif/viewer@4.12.1) (2021-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [4.12.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.11.2...@ohif/viewer@4.12.0) (2021-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Added support for WADO servers built with static hosting (e.g. S3) ([#2566](https://github.com/OHIF/Viewers/issues/2566)) ([a046c6c](https://github.com/OHIF/Viewers/commit/a046c6c9017a84e5ead71d5113b5e892333b4130))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.11.1...@ohif/viewer@4.11.2) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.11.0...@ohif/viewer@4.11.1) (2021-09-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [4.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.10.1...@ohif/viewer@4.11.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.10.0...@ohif/viewer@4.10.1) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [4.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.24...@ohif/viewer@4.10.0) (2021-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.24](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.23...@ohif/viewer@4.9.24) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.23](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.22...@ohif/viewer@4.9.23) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.22](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.21...@ohif/viewer@4.9.22) (2021-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove internet dependency for workbox (addressing [#2109](https://github.com/OHIF/Viewers/issues/2109)) ([#2151](https://github.com/OHIF/Viewers/issues/2151)) ([5f92f6e](https://github.com/OHIF/Viewers/commit/5f92f6ec7faa11d3fdc33ef8f22971a0bab87a36))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.20...@ohif/viewer@4.9.21) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.19...@ohif/viewer@4.9.20) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.18...@ohif/viewer@4.9.19) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.17...@ohif/viewer@4.9.18) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -333,6 +333,7 @@ describe('OHIF User Preferences', () => {
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
|
||||
// Header should be in "English (USA)""
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
@@ -560,6 +561,13 @@ describe('OHIF User Preferences', () => {
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Close Success Message overlay (if displayed)
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.sb-closeIcon').length > 0) {
|
||||
cy.get('.sb-closeIcon').click({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Open User Preferences modal again
|
||||
cy.openPreferences();
|
||||
|
||||
@@ -668,6 +676,13 @@ describe('OHIF User Preferences', () => {
|
||||
|
||||
// Save changes
|
||||
cy.get('@saveBtn').click();
|
||||
// Close Success Message overlay (if displayed)
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.sb-closeIcon').length > 0) {
|
||||
cy.get('.sb-closeIcon').click({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Open User Preferences modal
|
||||
cy.openPreferences();
|
||||
// Navigate to Window Level tab
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('OHIF Save Measurements', function() {
|
||||
// cy.get('.sb-closeIcon').click();
|
||||
// });
|
||||
|
||||
it('checks if warning message is displayed on measurements of unsupported tools', function() {
|
||||
/*it('checks if warning message is displayed on measurements of unsupported tools', function() {
|
||||
// Add measurement for unsupported tool in the viewport
|
||||
cy.addAngleMeasurement();
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('OHIF Save Measurements', function() {
|
||||
|
||||
// Close Measurements panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
});
|
||||
});*/
|
||||
|
||||
/*it('checks if measurements of unsupported tools were not saved', function() {
|
||||
// Add measurement for supported tool in the viewport
|
||||
|
||||
@@ -504,6 +504,9 @@ Cypress.Commands.add('resetUserHotkeyPreferences', () => {
|
||||
cy.get('@restoreBtn').click();
|
||||
});
|
||||
|
||||
// Click on Save Button
|
||||
cy.get('@saveBtn').click();
|
||||
|
||||
// Close Success Message overlay (if displayed)
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.sb-closeIcon').length > 0) {
|
||||
@@ -511,8 +514,6 @@ Cypress.Commands.add('resetUserHotkeyPreferences', () => {
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
// Click on Save Button
|
||||
cy.get('@saveBtn').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -525,6 +526,9 @@ Cypress.Commands.add('resetUserGeneralPreferences', () => {
|
||||
cy.get('@restoreBtn').click();
|
||||
});
|
||||
|
||||
// Click on Save Button
|
||||
cy.get('@saveBtn').click();
|
||||
|
||||
// Close Success Message overlay (if displayed)
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.sb-closeIcon').length > 0) {
|
||||
@@ -532,8 +536,6 @@ Cypress.Commands.add('resetUserGeneralPreferences', () => {
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
// Click on Save Button
|
||||
cy.get('@saveBtn').click();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "4.9.18",
|
||||
"version": "4.12.12",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -19,14 +19,12 @@
|
||||
"scripts": {
|
||||
"build:package": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.commonjs.js --progress",
|
||||
"build:viewer": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
|
||||
"build:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
|
||||
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
|
||||
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
|
||||
"build:viewer:package": "yarn run build:package",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"dev:viewer": "yarn run dev",
|
||||
"dev:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"start": "yarn run dev",
|
||||
"test:e2e": "cypress open",
|
||||
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
|
||||
@@ -49,28 +47,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^2.13.12",
|
||||
"@ohif/extension-cornerstone": "^2.10.9",
|
||||
"@ohif/extension-debugging": "^0.2.1",
|
||||
"@ohif/extension-dicom-html": "^1.3.10",
|
||||
"@ohif/core": "^2.16.7",
|
||||
"@ohif/extension-cornerstone": "^2.12.4",
|
||||
"@ohif/extension-debugging": "^0.3.1",
|
||||
"@ohif/extension-dicom-html": "^1.3.15",
|
||||
"@ohif/extension-dicom-microscopy": "^0.52.0",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.6",
|
||||
"@ohif/extension-dicom-rt": "^0.6.13",
|
||||
"@ohif/extension-dicom-segmentation": "^0.6.10",
|
||||
"@ohif/extension-dicom-pdf": "^1.1.1",
|
||||
"@ohif/extension-dicom-rt": "^0.7.6",
|
||||
"@ohif/extension-dicom-segmentation": "^0.7.6",
|
||||
"@ohif/extension-lesion-tracker": "^0.2.1",
|
||||
"@ohif/extension-vtk": "^1.11.13",
|
||||
"@ohif/extension-vtk": "^1.12.11",
|
||||
"@ohif/i18n": "^0.53.0",
|
||||
"@ohif/ui": "^1.9.2",
|
||||
"@ohif/ui": "^1.10.5",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-math": "^0.1.9",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dcmjs": "0.19.2",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@@ -1,28 +1,139 @@
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
enableGoogleCloudAdapter: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
dicomWeb: [],
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
disableServersCache: false,
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'closest',
|
||||
displaySetCount: 3,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
maxNumPrefetchRequests: 100,
|
||||
displayProgress: true,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
oidc: [
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Extensions should be able to suggest default values for these?
|
||||
// Or we can require that these be explicitly set
|
||||
hotkeys: [
|
||||
// ~ Global
|
||||
{
|
||||
// ~ REQUIRED
|
||||
// Authorization Server URL
|
||||
authority: 'https://accounts.google.com',
|
||||
client_id:
|
||||
'723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com',
|
||||
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
|
||||
response_type: 'id_token token',
|
||||
scope:
|
||||
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
|
||||
// ~ OPTIONAL
|
||||
post_logout_redirect_uri: '/logout-redirect.html',
|
||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||
automaticSilentRenew: true,
|
||||
revokeAccessTokenOnSignout: true,
|
||||
commandName: 'incrementActiveViewport',
|
||||
label: 'Next Viewport',
|
||||
keys: ['right'],
|
||||
},
|
||||
{
|
||||
commandName: 'decrementActiveViewport',
|
||||
label: 'Previous Viewport',
|
||||
keys: ['left'],
|
||||
},
|
||||
// Supported Keys: https://craig.is/killing/mice
|
||||
// ~ Cornerstone Extension
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
||||
{
|
||||
commandName: 'flipViewportVertical',
|
||||
label: 'Flip Horizontally',
|
||||
keys: ['h'],
|
||||
},
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
label: 'Flip Vertically',
|
||||
keys: ['v'],
|
||||
},
|
||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
|
||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
|
||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
|
||||
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
|
||||
// clearAnnotations
|
||||
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
|
||||
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
|
||||
// firstImage
|
||||
// lastImage
|
||||
{
|
||||
commandName: 'previousViewportDisplaySet',
|
||||
label: 'Previous Series',
|
||||
keys: ['pagedown'],
|
||||
},
|
||||
{
|
||||
commandName: 'nextViewportDisplaySet',
|
||||
label: 'Next Series',
|
||||
keys: ['pageup'],
|
||||
},
|
||||
// ~ Cornerstone Tools
|
||||
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
||||
// ~ Window level presets
|
||||
{
|
||||
commandName: 'windowLevelPreset1',
|
||||
label: 'W/L Preset 1',
|
||||
keys: ['1'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset2',
|
||||
label: 'W/L Preset 2',
|
||||
keys: ['2'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset3',
|
||||
label: 'W/L Preset 3',
|
||||
keys: ['3'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset4',
|
||||
label: 'W/L Preset 4',
|
||||
keys: ['4'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset5',
|
||||
label: 'W/L Preset 5',
|
||||
keys: ['5'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset6',
|
||||
label: 'W/L Preset 6',
|
||||
keys: ['6'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset7',
|
||||
label: 'W/L Preset 7',
|
||||
keys: ['7'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset8',
|
||||
label: 'W/L Preset 8',
|
||||
keys: ['8'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset9',
|
||||
label: 'W/L Preset 9',
|
||||
keys: ['9'],
|
||||
},
|
||||
],
|
||||
studyListFunctionsEnabled: true,
|
||||
cornerstoneExtensionConfig: {},
|
||||
// Following property limits number of simultaneous series metadata requests.
|
||||
// For http/1.x-only servers, set this to 5 or less to improve
|
||||
// on first meaningful display in viewer
|
||||
// If the server is particularly slow to respond to series metadata
|
||||
// requests as it extracts the metadata from raw files everytime,
|
||||
// try setting this to even lower value
|
||||
// Leave it undefined for no limit, suitable for HTTP/2 enabled servers
|
||||
// maxConcurrentMetadataRequests: 5,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'LocalStatic',
|
||||
wadoUriRoot: 'http://localhost:5000/',
|
||||
qidoRoot: 'http://localhost:5000',
|
||||
wadoRoot: 'http://localhost:5000/',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'thumbnail',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
staticWado: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,15 @@
|
||||
window.config = {
|
||||
routerBasename: '/pwa',
|
||||
showStudyList: true,
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'closest',
|
||||
displaySetCount: 3,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
displayProgress: true,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,17 @@
|
||||
// modules, so it's perfectly fine to serve this code to any browsers
|
||||
// (older browsers will just ignore it)
|
||||
//
|
||||
import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
|
||||
|
||||
// The workbox code is locally stored and can be updated
|
||||
// by running `npx workbox-cli copyLibraries platform/viewer/public/third_party/workbox`
|
||||
// from the root directory.
|
||||
// Since the workbox version may change with copyLibraries ensure that the
|
||||
// right version is used here and in `service-worker.js`
|
||||
import { Workbox } from './third_party/workbox/workbox-v5.1.4/workbox-window.dev.mjs';
|
||||
|
||||
// Alternatively use the internet version by commenting out the above import and
|
||||
// replace with this line and do the corresponding change in service-worker.js
|
||||
// import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
|
||||
|
||||
var supportsServiceWorker = 'serviceWorker' in navigator;
|
||||
var isNotLocalDevelopment =
|
||||
@@ -11,6 +21,7 @@ var isNotLocalDevelopment =
|
||||
|
||||
if (supportsServiceWorker && isNotLocalDevelopment) {
|
||||
const swFileLocation = (window.PUBLIC_URL || '/') + 'sw.js';
|
||||
|
||||
const wb = new Workbox(swFileLocation);
|
||||
|
||||
// Add an event listener to detect when the registered
|
||||
|
||||
+818
@@ -0,0 +1,818 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.backgroundSync = (function (exports, WorkboxError_js, logger_js, assert_js, getFriendlyURL_js, DBWrapper_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:background-sync:5.1.4'] && _();
|
||||
} catch (e) {}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
const DB_VERSION = 3;
|
||||
const DB_NAME = 'workbox-background-sync';
|
||||
const OBJECT_STORE_NAME = 'requests';
|
||||
const INDEXED_PROP = 'queueName';
|
||||
/**
|
||||
* A class to manage storing requests from a Queue in IndexedDB,
|
||||
* indexed by their queue name for easier access.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
class QueueStore {
|
||||
/**
|
||||
* Associates this instance with a Queue instance, so entries added can be
|
||||
* identified by their queue name.
|
||||
*
|
||||
* @param {string} queueName
|
||||
* @private
|
||||
*/
|
||||
constructor(queueName) {
|
||||
this._queueName = queueName;
|
||||
this._db = new DBWrapper_js.DBWrapper(DB_NAME, DB_VERSION, {
|
||||
onupgradeneeded: this._upgradeDb
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Append an entry last in the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Object} entry.requestData
|
||||
* @param {number} [entry.timestamp]
|
||||
* @param {Object} [entry.metadata]
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async pushEntry(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'pushEntry',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isType(entry.requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'pushEntry',
|
||||
paramName: 'entry.requestData'
|
||||
});
|
||||
} // Don't specify an ID since one is automatically generated.
|
||||
|
||||
|
||||
delete entry.id;
|
||||
entry.queueName = this._queueName;
|
||||
await this._db.add(OBJECT_STORE_NAME, entry);
|
||||
}
|
||||
/**
|
||||
* Prepend an entry first in the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Object} entry.requestData
|
||||
* @param {number} [entry.timestamp]
|
||||
* @param {Object} [entry.metadata]
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async unshiftEntry(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'unshiftEntry',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isType(entry.requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'unshiftEntry',
|
||||
paramName: 'entry.requestData'
|
||||
});
|
||||
}
|
||||
|
||||
const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
count: 1
|
||||
});
|
||||
|
||||
if (firstEntry) {
|
||||
// Pick an ID one less than the lowest ID in the object store.
|
||||
entry.id = firstEntry.id - 1;
|
||||
} else {
|
||||
// Otherwise let the auto-incrementor assign the ID.
|
||||
delete entry.id;
|
||||
}
|
||||
|
||||
entry.queueName = this._queueName;
|
||||
await this._db.add(OBJECT_STORE_NAME, entry);
|
||||
}
|
||||
/**
|
||||
* Removes and returns the last entry in the queue matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async popEntry() {
|
||||
return this._removeEntry({
|
||||
direction: 'prev'
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first entry in the queue matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async shiftEntry() {
|
||||
return this._removeEntry({
|
||||
direction: 'next'
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns all entries in the store matching the `queueName`.
|
||||
*
|
||||
* @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}
|
||||
* @return {Promise<Array<Object>>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async getAll() {
|
||||
return await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
index: INDEXED_PROP,
|
||||
query: IDBKeyRange.only(this._queueName)
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Deletes the entry for the given ID.
|
||||
*
|
||||
* WARNING: this method does not ensure the deleted enry belongs to this
|
||||
* queue (i.e. matches the `queueName`). But this limitation is acceptable
|
||||
* as this class is not publicly exposed. An additional check would make
|
||||
* this method slower than it needs to be.
|
||||
*
|
||||
* @private
|
||||
* @param {number} id
|
||||
*/
|
||||
|
||||
|
||||
async deleteEntry(id) {
|
||||
await this._db.delete(OBJECT_STORE_NAME, id);
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first or last entry in the queue (based on the
|
||||
* `direction` argument) matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _removeEntry({
|
||||
direction
|
||||
}) {
|
||||
const [entry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
direction,
|
||||
index: INDEXED_PROP,
|
||||
query: IDBKeyRange.only(this._queueName),
|
||||
count: 1
|
||||
});
|
||||
|
||||
if (entry) {
|
||||
await this.deleteEntry(entry.id);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Upgrades the database given an `upgradeneeded` event.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_upgradeDb(event) {
|
||||
const db = event.target.result;
|
||||
|
||||
if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
|
||||
if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) {
|
||||
db.deleteObjectStore(OBJECT_STORE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {
|
||||
autoIncrement: true,
|
||||
keyPath: 'id'
|
||||
});
|
||||
objStore.createIndex(INDEXED_PROP, INDEXED_PROP, {
|
||||
unique: false
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
const serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive'];
|
||||
/**
|
||||
* A class to make it easier to serialize and de-serialize requests so they
|
||||
* can be stored in IndexedDB.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
class StorableRequest {
|
||||
/**
|
||||
* Accepts an object of request data that can be used to construct a
|
||||
* `Request` but can also be stored in IndexedDB.
|
||||
*
|
||||
* @param {Object} requestData An object of request data that includes the
|
||||
* `url` plus any relevant properties of
|
||||
* [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}.
|
||||
* @private
|
||||
*/
|
||||
constructor(requestData) {
|
||||
{
|
||||
assert_js.assert.isType(requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'StorableRequest',
|
||||
funcName: 'constructor',
|
||||
paramName: 'requestData'
|
||||
});
|
||||
assert_js.assert.isType(requestData.url, 'string', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'StorableRequest',
|
||||
funcName: 'constructor',
|
||||
paramName: 'requestData.url'
|
||||
});
|
||||
} // If the request's mode is `navigate`, convert it to `same-origin` since
|
||||
// navigation requests can't be constructed via script.
|
||||
|
||||
|
||||
if (requestData['mode'] === 'navigate') {
|
||||
requestData['mode'] = 'same-origin';
|
||||
}
|
||||
|
||||
this._requestData = requestData;
|
||||
}
|
||||
/**
|
||||
* Converts a Request object to a plain object that can be structured
|
||||
* cloned or JSON-stringified.
|
||||
*
|
||||
* @param {Request} request
|
||||
* @return {Promise<StorableRequest>}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
static async fromRequest(request) {
|
||||
const requestData = {
|
||||
url: request.url,
|
||||
headers: {}
|
||||
}; // Set the body if present.
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
// Use ArrayBuffer to support non-text request bodies.
|
||||
// NOTE: we can't use Blobs becuse Safari doesn't support storing
|
||||
// Blobs in IndexedDB in some cases:
|
||||
// https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
|
||||
requestData.body = await request.clone().arrayBuffer();
|
||||
} // Convert the headers from an iterable to an object.
|
||||
|
||||
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
requestData.headers[key] = value;
|
||||
} // Add all other serializable request properties
|
||||
|
||||
|
||||
for (const prop of serializableProperties) {
|
||||
if (request[prop] !== undefined) {
|
||||
requestData[prop] = request[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return new StorableRequest(requestData);
|
||||
}
|
||||
/**
|
||||
* Returns a deep clone of the instances `_requestData` object.
|
||||
*
|
||||
* @return {Object}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
toObject() {
|
||||
const requestData = Object.assign({}, this._requestData);
|
||||
requestData.headers = Object.assign({}, this._requestData.headers);
|
||||
|
||||
if (requestData.body) {
|
||||
requestData.body = requestData.body.slice(0);
|
||||
}
|
||||
|
||||
return requestData;
|
||||
}
|
||||
/**
|
||||
* Converts this instance to a Request.
|
||||
*
|
||||
* @return {Request}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
toRequest() {
|
||||
return new Request(this._requestData.url, this._requestData);
|
||||
}
|
||||
/**
|
||||
* Creates and returns a deep clone of the instance.
|
||||
*
|
||||
* @return {StorableRequest}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
clone() {
|
||||
return new StorableRequest(this.toObject());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
const TAG_PREFIX = 'workbox-background-sync';
|
||||
const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
|
||||
|
||||
const queueNames = new Set();
|
||||
/**
|
||||
* Converts a QueueStore entry into the format exposed by Queue. This entails
|
||||
* converting the request data into a real request and omitting the `id` and
|
||||
* `queueName` properties.
|
||||
*
|
||||
* @param {Object} queueStoreEntry
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
|
||||
const convertEntry = queueStoreEntry => {
|
||||
const queueEntry = {
|
||||
request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
|
||||
timestamp: queueStoreEntry.timestamp
|
||||
};
|
||||
|
||||
if (queueStoreEntry.metadata) {
|
||||
queueEntry.metadata = queueStoreEntry.metadata;
|
||||
}
|
||||
|
||||
return queueEntry;
|
||||
};
|
||||
/**
|
||||
* A class to manage storing failed requests in IndexedDB and retrying them
|
||||
* later. All parts of the storing and replaying process are observable via
|
||||
* callbacks.
|
||||
*
|
||||
* @memberof module:workbox-background-sync
|
||||
*/
|
||||
|
||||
|
||||
class Queue {
|
||||
/**
|
||||
* Creates an instance of Queue with the given options
|
||||
*
|
||||
* @param {string} name The unique name for this queue. This name must be
|
||||
* unique as it's used to register sync events and store requests
|
||||
* in IndexedDB specific to this instance. An error will be thrown if
|
||||
* a duplicate name is detected.
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.onSync] A function that gets invoked whenever
|
||||
* the 'sync' event fires. The function is invoked with an object
|
||||
* containing the `queue` property (referencing this instance), and you
|
||||
* can use the callback to customize the replay behavior of the queue.
|
||||
* When not set the `replayRequests()` method is called.
|
||||
* Note: if the replay fails after a sync event, make sure you throw an
|
||||
* error, so the browser knows to retry the sync event later.
|
||||
* @param {number} [options.maxRetentionTime=7 days] The amount of time (in
|
||||
* minutes) a request may be retried. After this amount of time has
|
||||
* passed, the request will be deleted from the queue.
|
||||
*/
|
||||
constructor(name, {
|
||||
onSync,
|
||||
maxRetentionTime
|
||||
} = {}) {
|
||||
this._syncInProgress = false;
|
||||
this._requestsAddedDuringSync = false; // Ensure the store name is not already being used
|
||||
|
||||
if (queueNames.has(name)) {
|
||||
throw new WorkboxError_js.WorkboxError('duplicate-queue-name', {
|
||||
name
|
||||
});
|
||||
} else {
|
||||
queueNames.add(name);
|
||||
}
|
||||
|
||||
this._name = name;
|
||||
this._onSync = onSync || this.replayRequests;
|
||||
this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
|
||||
this._queueStore = new QueueStore(this._name);
|
||||
|
||||
this._addSyncListener();
|
||||
}
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
/**
|
||||
* Stores the passed request in IndexedDB (with its timestamp and any
|
||||
* metadata) at the end of the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request The request to store in the queue.
|
||||
* @param {Object} [entry.metadata] Any metadata you want associated with the
|
||||
* stored request. When requests are replayed you'll have access to this
|
||||
* metadata object in case you need to modify the request beforehand.
|
||||
* @param {number} [entry.timestamp] The timestamp (Epoch time in
|
||||
* milliseconds) when the request was first added to the queue. This is
|
||||
* used along with `maxRetentionTime` to remove outdated requests. In
|
||||
* general you don't need to set this value, as it's automatically set
|
||||
* for you (defaulting to `Date.now()`), but you can update it if you
|
||||
* don't want particular requests to expire.
|
||||
*/
|
||||
|
||||
|
||||
async pushRequest(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'pushRequest',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isInstance(entry.request, Request, {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'pushRequest',
|
||||
paramName: 'entry.request'
|
||||
});
|
||||
}
|
||||
|
||||
await this._addRequest(entry, 'push');
|
||||
}
|
||||
/**
|
||||
* Stores the passed request in IndexedDB (with its timestamp and any
|
||||
* metadata) at the beginning of the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request The request to store in the queue.
|
||||
* @param {Object} [entry.metadata] Any metadata you want associated with the
|
||||
* stored request. When requests are replayed you'll have access to this
|
||||
* metadata object in case you need to modify the request beforehand.
|
||||
* @param {number} [entry.timestamp] The timestamp (Epoch time in
|
||||
* milliseconds) when the request was first added to the queue. This is
|
||||
* used along with `maxRetentionTime` to remove outdated requests. In
|
||||
* general you don't need to set this value, as it's automatically set
|
||||
* for you (defaulting to `Date.now()`), but you can update it if you
|
||||
* don't want particular requests to expire.
|
||||
*/
|
||||
|
||||
|
||||
async unshiftRequest(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'unshiftRequest',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isInstance(entry.request, Request, {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'unshiftRequest',
|
||||
paramName: 'entry.request'
|
||||
});
|
||||
}
|
||||
|
||||
await this._addRequest(entry, 'unshift');
|
||||
}
|
||||
/**
|
||||
* Removes and returns the last request in the queue (along with its
|
||||
* timestamp and any metadata). The returned object takes the form:
|
||||
* `{request, timestamp, metadata}`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
|
||||
|
||||
async popRequest() {
|
||||
return this._removeRequest('pop');
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first request in the queue (along with its
|
||||
* timestamp and any metadata). The returned object takes the form:
|
||||
* `{request, timestamp, metadata}`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
|
||||
|
||||
async shiftRequest() {
|
||||
return this._removeRequest('shift');
|
||||
}
|
||||
/**
|
||||
* Returns all the entries that have not expired (per `maxRetentionTime`).
|
||||
* Any expired entries are removed from the queue.
|
||||
*
|
||||
* @return {Promise<Array<Object>>}
|
||||
*/
|
||||
|
||||
|
||||
async getAll() {
|
||||
const allEntries = await this._queueStore.getAll();
|
||||
const now = Date.now();
|
||||
const unexpiredEntries = [];
|
||||
|
||||
for (const entry of allEntries) {
|
||||
// Ignore requests older than maxRetentionTime. Call this function
|
||||
// recursively until an unexpired request is found.
|
||||
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
||||
|
||||
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
||||
await this._queueStore.deleteEntry(entry.id);
|
||||
} else {
|
||||
unexpiredEntries.push(convertEntry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
return unexpiredEntries;
|
||||
}
|
||||
/**
|
||||
* Adds the entry to the QueueStore and registers for a sync event.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request
|
||||
* @param {Object} [entry.metadata]
|
||||
* @param {number} [entry.timestamp=Date.now()]
|
||||
* @param {string} operation ('push' or 'unshift')
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _addRequest({
|
||||
request,
|
||||
metadata,
|
||||
timestamp = Date.now()
|
||||
}, operation) {
|
||||
const storableRequest = await StorableRequest.fromRequest(request.clone());
|
||||
const entry = {
|
||||
requestData: storableRequest.toObject(),
|
||||
timestamp
|
||||
}; // Only include metadata if it's present.
|
||||
|
||||
if (metadata) {
|
||||
entry.metadata = metadata;
|
||||
}
|
||||
|
||||
await this._queueStore[`${operation}Entry`](entry);
|
||||
|
||||
{
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`);
|
||||
} // Don't register for a sync if we're in the middle of a sync. Instead,
|
||||
// we wait until the sync is complete and call register if
|
||||
// `this._requestsAddedDuringSync` is true.
|
||||
|
||||
|
||||
if (this._syncInProgress) {
|
||||
this._requestsAddedDuringSync = true;
|
||||
} else {
|
||||
await this.registerSync();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first or last (depending on `operation`) entry
|
||||
* from the QueueStore that's not older than the `maxRetentionTime`.
|
||||
*
|
||||
* @param {string} operation ('pop' or 'shift')
|
||||
* @return {Object|undefined}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _removeRequest(operation) {
|
||||
const now = Date.now();
|
||||
const entry = await this._queueStore[`${operation}Entry`]();
|
||||
|
||||
if (entry) {
|
||||
// Ignore requests older than maxRetentionTime. Call this function
|
||||
// recursively until an unexpired request is found.
|
||||
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
||||
|
||||
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
||||
return this._removeRequest(operation);
|
||||
}
|
||||
|
||||
return convertEntry(entry);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loops through each request in the queue and attempts to re-fetch it.
|
||||
* If any request fails to re-fetch, it's put back in the same position in
|
||||
* the queue (which registers a retry for the next sync event).
|
||||
*/
|
||||
|
||||
|
||||
async replayRequests() {
|
||||
let entry;
|
||||
|
||||
while (entry = await this.shiftRequest()) {
|
||||
try {
|
||||
await fetch(entry.request.clone());
|
||||
|
||||
if ("dev" !== 'production') {
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `has been replayed in queue '${this._name}'`);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.unshiftRequest(entry);
|
||||
|
||||
{
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `failed to replay, putting it back in queue '${this._name}'`);
|
||||
}
|
||||
|
||||
throw new WorkboxError_js.WorkboxError('queue-replay-failed', {
|
||||
name: this._name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
logger_js.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers a sync event with a tag unique to this instance.
|
||||
*/
|
||||
|
||||
|
||||
async registerSync() {
|
||||
if ('sync' in self.registration) {
|
||||
try {
|
||||
await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
|
||||
} catch (err) {
|
||||
// This means the registration failed for some reason, possibly due to
|
||||
// the user disabling it.
|
||||
{
|
||||
logger_js.logger.warn(`Unable to register sync event for '${this._name}'.`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* In sync-supporting browsers, this adds a listener for the sync event.
|
||||
* In non-sync-supporting browsers, this will retry the queue on service
|
||||
* worker startup.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_addSyncListener() {
|
||||
if ('sync' in self.registration) {
|
||||
self.addEventListener('sync', event => {
|
||||
if (event.tag === `${TAG_PREFIX}:${this._name}`) {
|
||||
{
|
||||
logger_js.logger.log(`Background sync for tag '${event.tag}'` + `has been received`);
|
||||
}
|
||||
|
||||
const syncComplete = async () => {
|
||||
this._syncInProgress = true;
|
||||
let syncError;
|
||||
|
||||
try {
|
||||
await this._onSync({
|
||||
queue: this
|
||||
});
|
||||
} catch (error) {
|
||||
syncError = error; // Rethrow the error. Note: the logic in the finally clause
|
||||
// will run before this gets rethrown.
|
||||
|
||||
throw syncError;
|
||||
} finally {
|
||||
// New items may have been added to the queue during the sync,
|
||||
// so we need to register for a new sync if that's happened...
|
||||
// Unless there was an error during the sync, in which
|
||||
// case the browser will automatically retry later, as long
|
||||
// as `event.lastChance` is not true.
|
||||
if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
|
||||
await this.registerSync();
|
||||
}
|
||||
|
||||
this._syncInProgress = false;
|
||||
this._requestsAddedDuringSync = false;
|
||||
}
|
||||
};
|
||||
|
||||
event.waitUntil(syncComplete());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
{
|
||||
logger_js.logger.log(`Background sync replaying without background sync event`);
|
||||
} // If the browser doesn't support background sync, retry
|
||||
// every time the service worker starts up as a fallback.
|
||||
|
||||
|
||||
this._onSync({
|
||||
queue: this
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the set of queue names. This is primarily used to reset the list
|
||||
* of queue names in tests.
|
||||
*
|
||||
* @return {Set}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
static get _queueNames() {
|
||||
return queueNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
/**
|
||||
* A class implementing the `fetchDidFail` lifecycle callback. This makes it
|
||||
* easier to add failed requests to a background sync Queue.
|
||||
*
|
||||
* @memberof module:workbox-background-sync
|
||||
*/
|
||||
|
||||
class BackgroundSyncPlugin {
|
||||
/**
|
||||
* @param {string} name See the [Queue]{@link module:workbox-background-sync.Queue}
|
||||
* documentation for parameter details.
|
||||
* @param {Object} [options] See the
|
||||
* [Queue]{@link module:workbox-background-sync.Queue} documentation for
|
||||
* parameter details.
|
||||
*/
|
||||
constructor(name, options) {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Request} options.request
|
||||
* @private
|
||||
*/
|
||||
this.fetchDidFail = async ({
|
||||
request
|
||||
}) => {
|
||||
await this._queue.pushRequest({
|
||||
request
|
||||
});
|
||||
};
|
||||
|
||||
this._queue = new Queue(name, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.BackgroundSyncPlugin = BackgroundSyncPlugin;
|
||||
exports.Queue = Queue;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-background-sync.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-background-sync.dev.js","sources":["../_version.js","../lib/QueueStore.js","../lib/StorableRequest.js","../Queue.js","../BackgroundSyncPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:background-sync:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport '../_version.js';\nconst DB_VERSION = 3;\nconst DB_NAME = 'workbox-background-sync';\nconst OBJECT_STORE_NAME = 'requests';\nconst INDEXED_PROP = 'queueName';\n/**\n * A class to manage storing requests from a Queue in IndexedDB,\n * indexed by their queue name for easier access.\n *\n * @private\n */\nexport class QueueStore {\n /**\n * Associates this instance with a Queue instance, so entries added can be\n * identified by their queue name.\n *\n * @param {string} queueName\n * @private\n */\n constructor(queueName) {\n this._queueName = queueName;\n this._db = new DBWrapper(DB_NAME, DB_VERSION, {\n onupgradeneeded: this._upgradeDb,\n });\n }\n /**\n * Append an entry last in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async pushEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry.requestData',\n });\n }\n // Don't specify an ID since one is automatically generated.\n delete entry.id;\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Prepend an entry first in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async unshiftEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry.requestData',\n });\n }\n const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {\n count: 1,\n });\n if (firstEntry) {\n // Pick an ID one less than the lowest ID in the object store.\n entry.id = firstEntry.id - 1;\n }\n else {\n // Otherwise let the auto-incrementor assign the ID.\n delete entry.id;\n }\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Removes and returns the last entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async popEntry() {\n return this._removeEntry({ direction: 'prev' });\n }\n /**\n * Removes and returns the first entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async shiftEntry() {\n return this._removeEntry({ direction: 'next' });\n }\n /**\n * Returns all entries in the store matching the `queueName`.\n *\n * @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}\n * @return {Promise<Array<Object>>}\n * @private\n */\n async getAll() {\n return await this._db.getAllMatching(OBJECT_STORE_NAME, {\n index: INDEXED_PROP,\n query: IDBKeyRange.only(this._queueName),\n });\n }\n /**\n * Deletes the entry for the given ID.\n *\n * WARNING: this method does not ensure the deleted enry belongs to this\n * queue (i.e. matches the `queueName`). But this limitation is acceptable\n * as this class is not publicly exposed. An additional check would make\n * this method slower than iLine truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s,i,n){"use strict";try{self["workbox:background-sync:5.1.4"]&&_()}catch(t){}class a{constructor(t){this.t=t,this.s=new n.DBWrapper("workbox-background-sync",3,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching("requests",{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching("requests",{index:"queueName",query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete("requests",t)}async h({direction:t}){const[e]=await this.s.getAllMatching("requests",{direction:t,index:"queueName",query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<3&&e.objectStoreNames.contains("requests")&&e.deleteObjectStore("requests");e.createObjectStore("requests",{autoIncrement:!0,keyPath:"id"}).createIndex("queueName","queueName",{unique:!1})}}const r=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class c{constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.u=t}static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of r)void 0!==t[s]&&(e[s]=t[s]);return new c(e)}toObject(){const t=Object.assign({},this.u);return t.headers=Object.assign({},this.u.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.u.url,this.u)}clone(){return new c(this.toObject())}}const h=new Set,u=t=>{const e={request:new c(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};class o{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(this.o=!1,this.q=!1,h.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});h.add(t),this.l=t,this.m=s||this.replayRequests,this.p=i||10080,this.g=new a(this.l),this.R()}get name(){return this.l}async pushRequest(t){await this.k(t,"push")}async unshiftRequest(t){await this.k(t,"unshift")}async popRequest(){return this.D("pop")}async shiftRequest(){return this.D("shift")}async getAll(){const t=await this.g.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.p*1e3;e-i.timestamp>t?await this.g.deleteEntry(i.id):s.push(u(i))}return s}async k({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await c.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.g[i+"Entry"](n),this.o?this.q=!0:await this.registerSync()}async D(t){const e=Date.now(),s=await this.g[t+"Entry"]();if(s){const i=60*this.p*1e3;return e-s.timestamp>i?this.D(t):u(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.l})}}async registerSync(){if("sync"in self.registration)try{await self.registration.sync.register("workbox-background-sync:"+this.l)}catch(t){}}R(){"sync"in self.registration?self.addEventListener("sync",t=>{if(t.tag==="workbox-background-sync:"+this.l){const e=async()=>{let e;this.o=!0;try{await this.m({queue:this})}catch(t){throw e=t,e}finally{!this.q||e&&!t.lastChance||await this.registerSync(),this.o=!1,this.q=!1}};t.waitUntil(e())}}):this.m({queue:this})}static get _(){return h}}return t.BackgroundSyncPlugin=class{constructor(t,e){this.fetchDidFail=async({request:t})=>{await this.v.pushRequest({request:t})},this.v=new o(t,e)}},t.Queue=o,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
|
||||
//# sourceMappingURL=workbox-background-sync.prod.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-background-sync.prod.js","sources":["../_version.js","../lib/QueueStore.js","../lib/StorableRequest.js","../Queue.js","../BackgroundSyncPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:background-sync:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport '../_version.js';\nconst DB_VERSION = 3;\nconst DB_NAME = 'workbox-background-sync';\nconst OBJECT_STORE_NAME = 'requests';\nconst INDEXED_PROP = 'queueName';\n/**\n * A class to manage storing requests from a Queue in IndexedDB,\n * indexed by their queue name for easier access.\n *\n * @private\n */\nexport class QueueStore {\n /**\n * Associates this instance with a Queue instance, so entries added can be\n * identified by their queue name.\n *\n * @param {string} queueName\n * @private\n */\n constructor(queueName) {\n this._queueName = queueName;\n this._db = new DBWrapper(DB_NAME, DB_VERSION, {\n onupgradeneeded: this._upgradeDb,\n });\n }\n /**\n * Append an entry last in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async pushEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry.requestData',\n });\n }\n // Don't specify an ID since one is automatically generated.\n delete entry.id;\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Prepend an entry first in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async unshiftEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry.requestData',\n });\n }\n const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {\n count: 1,\n });\n if (firstEntry) {\n // Pick an ID one less than the lowest ID in the object store.\n entry.id = firstEntry.id - 1;\n }\n else {\n // Otherwise let the auto-incrementor assign the ID.\n delete entry.id;\n }\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Removes and returns the last entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async popEntry() {\n return this._removeEntry({ direction: 'prev' });\n }\n /**\n * Removes and returns the first entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async shiftEntry() {\n return this._removeEntry({ direction: 'next' });\n }\n /**\n * Returns all entries in the store matching the `queueName`.\n *\n * @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}\n * @return {Promise<Array<Object>>}\n * @private\n */\n async getAll() {\n return await this._db.getAllMatching(OBJECT_STORE_NAME, {\n index: INDEXED_PROP,\n query: IDBKeyRange.only(this._queueName),\n });\n }\n /**\n * Deletes the entry for the given ID.\n *\n * WARNING: this method does not ensure the deleted enry belongs to this\n * queue (i.e. matches the `queueName`). But this limitation is acceptable\n * as this class is not publicly exposed. An additional check would make\n * this method slower than Line truncated
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.broadcastUpdate = (function (exports, assert_js, timeout_js, resultingClientExists_js, logger_js, WorkboxError_js, dontWaitFor_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:broadcast-update:5.1.4'] && _();
|
||||
} catch (e) {}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
/**
|
||||
* Given two `Response's`, compares several header values to see if they are
|
||||
* the same or not.
|
||||
*
|
||||
* @param {Response} firstResponse
|
||||
* @param {Response} secondResponse
|
||||
* @param {Array<string>} headersToCheck
|
||||
* @return {boolean}
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
const responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {
|
||||
{
|
||||
if (!(firstResponse instanceof Response && secondResponse instanceof Response)) {
|
||||
throw new WorkboxError_js.WorkboxError('invalid-responses-are-same-args');
|
||||
}
|
||||
}
|
||||
|
||||
const atLeastOneHeaderAvailable = headersToCheck.some(header => {
|
||||
return firstResponse.headers.has(header) && secondResponse.headers.has(header);
|
||||
});
|
||||
|
||||
if (!atLeastOneHeaderAvailable) {
|
||||
{
|
||||
logger_js.logger.warn(`Unable to determine where the response has been updated ` + `because none of the headers that would be checked are present.`);
|
||||
logger_js.logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);
|
||||
} // Just return true, indicating the that responses are the same, since we
|
||||
// can't determine otherwise.
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return headersToCheck.every(header => {
|
||||
const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header);
|
||||
const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header);
|
||||
return headerStateComparison && headerValueComparison;
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';
|
||||
const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';
|
||||
const DEFAULT_HEADERS_TO_CHECK = ['content-length', 'etag', 'last-modified'];
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
// TODO(philipwalton): remove once this Safari bug fix has been released.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=201169
|
||||
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
/**
|
||||
* Generates the default payload used in update messages. By default the
|
||||
* payload includes the `cacheName` and `updatedURL` fields.
|
||||
*
|
||||
* @return Object
|
||||
* @private
|
||||
*/
|
||||
|
||||
function defaultPayloadGenerator(data) {
|
||||
return {
|
||||
cacheName: data.cacheName,
|
||||
updatedURL: data.request.url
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Uses the `postMessage()` API to inform any open windows/tabs when a cached
|
||||
* response has been updated.
|
||||
*
|
||||
* For efficiency's sake, the underlying response bodies are not compared;
|
||||
* only specific response headers are checked.
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
|
||||
class BroadcastCacheUpdate {
|
||||
/**
|
||||
* Construct a BroadcastCacheUpdate instance with a specific `channelName` to
|
||||
* broadcast messages on
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
||||
* A list of headers that will be used to determine whether the responses
|
||||
* differ.
|
||||
* @param {string} [options.generatePayload] A function whose return value
|
||||
* will be used as the `payload` field in any cache update messages sent
|
||||
* to the window clients.
|
||||
*/
|
||||
constructor({
|
||||
headersToCheck,
|
||||
generatePayload
|
||||
} = {}) {
|
||||
this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK;
|
||||
this._generatePayload = generatePayload || defaultPayloadGenerator;
|
||||
}
|
||||
/**
|
||||
* Compares two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* and sends a message (via `postMessage()`) to all window clients if the
|
||||
* responses differ (note: neither of the Responses can be
|
||||
* {@link http://stackoverflow.com/questions/39109789|opaque}).
|
||||
*
|
||||
* The message that's posted has the following format (where `payload` can
|
||||
* be customized via the `generatePayload` option the instance is created
|
||||
* with):
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* type: 'CACHE_UPDATED',
|
||||
* meta: 'workbox-broadcast-update',
|
||||
* payload: {
|
||||
* cacheName: 'the-cache-name',
|
||||
* updatedURL: 'https://example.com/'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Response} [options.oldResponse] Cached response to compare.
|
||||
* @param {Response} options.newResponse Possibly updated response to compare.
|
||||
* @param {Request} options.request The request.
|
||||
* @param {string} options.cacheName Name of the cache the responses belong
|
||||
* to. This is included in the broadcast message.
|
||||
* @param {Event} [options.event] event An optional event that triggered
|
||||
* this possible cache update.
|
||||
* @return {Promise} Resolves once the update is sent.
|
||||
*/
|
||||
|
||||
|
||||
async notifyIfUpdated(options) {
|
||||
{
|
||||
assert_js.assert.isType(options.cacheName, 'string', {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'cacheName'
|
||||
});
|
||||
assert_js.assert.isInstance(options.newResponse, Response, {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'newResponse'
|
||||
});
|
||||
assert_js.assert.isInstance(options.request, Request, {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'request'
|
||||
});
|
||||
} // Without two responses there is nothing to compare.
|
||||
|
||||
|
||||
if (!options.oldResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responsesAreSame(options.oldResponse, options.newResponse, this._headersToCheck)) {
|
||||
{
|
||||
logger_js.logger.log(`Newer response found (and cached) for:`, options.request.url);
|
||||
}
|
||||
|
||||
const messageData = {
|
||||
type: CACHE_UPDATED_MESSAGE_TYPE,
|
||||
meta: CACHE_UPDATED_MESSAGE_META,
|
||||
payload: this._generatePayload(options)
|
||||
}; // For navigation requests, wait until the new window client exists
|
||||
// before sending the message
|
||||
|
||||
if (options.request.mode === 'navigate') {
|
||||
let resultingClientId;
|
||||
|
||||
if (options.event instanceof FetchEvent) {
|
||||
resultingClientId = options.event.resultingClientId;
|
||||
}
|
||||
|
||||
const resultingWin = await resultingClientExists_js.resultingClientExists(resultingClientId); // Safari does not currently implement postMessage buffering and
|
||||
// there's no good way to feature detect that, so to increase the
|
||||
// chances of the message being delivered in Safari, we add a timeout.
|
||||
// We also do this if `resultingClientExists()` didn't return a client,
|
||||
// which means it timed out, so it's worth waiting a bit longer.
|
||||
|
||||
if (!resultingWin || isSafari) {
|
||||
// 3500 is chosen because (according to CrUX data) 80% of mobile
|
||||
// websites hit the DOMContentLoaded event in less than 3.5 seconds.
|
||||
// And presumably sites implementing service worker are on the
|
||||
// higher end of the performance spectrum.
|
||||
await timeout_js.timeout(3500);
|
||||
}
|
||||
}
|
||||
|
||||
const windows = await self.clients.matchAll({
|
||||
type: 'window'
|
||||
});
|
||||
|
||||
for (const win of windows) {
|
||||
win.postMessage(messageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
/**
|
||||
* This plugin will automatically broadcast a message whenever a cached response
|
||||
* is updated.
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
class BroadcastUpdatePlugin {
|
||||
/**
|
||||
* Construct a BroadcastCacheUpdate instance with the passed options and
|
||||
* calls its [`notifyIfUpdated()`]{@link module:workbox-broadcast-update.BroadcastCacheUpdate~notifyIfUpdated}
|
||||
* method whenever the plugin's `cacheDidUpdate` callback is invoked.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
||||
* A list of headers that will be used to determine whether the responses
|
||||
* differ.
|
||||
* @param {string} [options.generatePayload] A function whose return value
|
||||
* will be used as the `payload` field in any cache update messages sent
|
||||
* to the window clients.
|
||||
*/
|
||||
constructor(options) {
|
||||
/**
|
||||
* A "lifecycle" callback that will be triggered automatically by the
|
||||
* `workbox-sw` and `workbox-runtime-caching` handlers when an entry is
|
||||
* added to a cache.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The input object to this function.
|
||||
* @param {string} options.cacheName Name of the cache being updated.
|
||||
* @param {Response} [options.oldResponse] The previous cached value, if any.
|
||||
* @param {Response} options.newResponse The new value in the cache.
|
||||
* @param {Request} options.request The request that triggered the update.
|
||||
* @param {Request} [options.event] The event that triggered the update.
|
||||
*/
|
||||
this.cacheDidUpdate = async options => {
|
||||
dontWaitFor_js.dontWaitFor(this._broadcastUpdate.notifyIfUpdated(options));
|
||||
};
|
||||
|
||||
this._broadcastUpdate = new BroadcastCacheUpdate(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.BroadcastCacheUpdate = BroadcastCacheUpdate;
|
||||
exports.BroadcastUpdatePlugin = BroadcastUpdatePlugin;
|
||||
exports.responsesAreSame = responsesAreSame;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-broadcast-update.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-broadcast-update.dev.js","sources":["../_version.js","../responsesAreSame.js","../utils/constants.js","../BroadcastCacheUpdate.js","../BroadcastUpdatePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:broadcast-update:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * Given two `Response's`, compares several header values to see if they are\n * the same or not.\n *\n * @param {Response} firstResponse\n * @param {Response} secondResponse\n * @param {Array<string>} headersToCheck\n * @return {boolean}\n *\n * @memberof module:workbox-broadcast-update\n */\nconst responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {\n if (process.env.NODE_ENV !== 'production') {\n if (!(firstResponse instanceof Response &&\n secondResponse instanceof Response)) {\n throw new WorkboxError('invalid-responses-are-same-args');\n }\n }\n const atLeastOneHeaderAvailable = headersToCheck.some((header) => {\n return firstResponse.headers.has(header) &&\n secondResponse.headers.has(header);\n });\n if (!atLeastOneHeaderAvailable) {\n if (process.env.NODE_ENV !== 'production') {\n logger.warn(`Unable to determine where the response has been updated ` +\n `because none of the headers that would be checked are present.`);\n logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);\n }\n // Just return true, indicating the that responses are the same, since we\n // can't determine otherwise.\n return true;\n }\n return headersToCheck.every((header) => {\n const headerStateComparison = firstResponse.headers.has(header) ===\n secondResponse.headers.has(header);\n const headerValueComparison = firstResponse.headers.get(header) ===\n secondResponse.headers.get(header);\n return headerStateComparison && headerValueComparison;\n });\n};\nexport { responsesAreSame };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nexport const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';\nexport const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';\nexport const DEFAULT_HEADERS_TO_CHECK = [\n 'content-length',\n 'etag',\n 'last-modified',\n];\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { timeout } from 'workbox-core/_private/timeout.js';\nimport { resultingClientExists } from 'workbox-core/_private/resultingClientExists.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { responsesAreSame } from './responsesAreSame.js';\nimport { CACHE_UPDATED_MESSAGE_TYPE, CACHE_UPDATED_MESSAGE_META, DEFAULT_HEADERS_TO_CHECK } from './utils/constants.js';\nimport './_version.js';\n// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser\n// TODO(philipwalton): remove once this Safari bug fix has been released.\n// https://bugs.webkit.org/show_bug.cgi?id=201169\nconst isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n/**\n * Generates the default payload used in update messages. By default the\n * payload includes the `cacheName` and `updatedURL` fields.\n *\n * @return Object\n * @private\n */\nfunction defaultPayloadGenerator(data) {\n return {\n cacheName: data.cacheName,\n updatedURL: data.request.url,\n };\n}\n/**\n * Uses the `postMessage()` API to inform any open windows/tabs when a cached\n * response has been updated.\n *\n * For efficiency's sake, the underlying response bodies are not compared;\n * only specific response headers are checked.\n *\n * @memberof module:workbox-broadcast-update\n */\nclass BroadcastCacheUpdate {\n /**\n * Construct a BroadcastCacheUpdate instance with a specific `channelName` to\n * broadcast messages on\n *\n * @param {Object} options\n * @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]\n * A list of headers that will be used to determine whether the responses\n * differ.\n * @param {string} [options.generatePayload] A function whose return value\n * will be used as the `payload` field in any cache update messaLine truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(t,a,o,s){"use strict";try{self["workbox:broadcast-update:5.1.4"]&&_()}catch(t){}const e=(t,a,o)=>!o.some(o=>t.headers.has(o)&&a.headers.has(o))||o.every(o=>{const s=t.headers.has(o)===a.headers.has(o),e=t.headers.get(o)===a.headers.get(o);return s&&e}),n=["content-length","etag","last-modified"],i=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function c(t){return{cacheName:t.cacheName,updatedURL:t.request.url}}class r{constructor({headersToCheck:t,generatePayload:a}={}){this.t=t||n,this.o=a||c}async notifyIfUpdated(t){if(t.oldResponse&&!e(t.oldResponse,t.newResponse,this.t)){const s={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:this.o(t)};if("navigate"===t.request.mode){let s;t.event instanceof FetchEvent&&(s=t.event.resultingClientId);await o.resultingClientExists(s)&&!i||await a.timeout(3500)}const e=await self.clients.matchAll({type:"window"});for(const t of e)t.postMessage(s)}}}return t.BroadcastCacheUpdate=r,t.BroadcastUpdatePlugin=class{constructor(t){this.cacheDidUpdate=async t=>{s.dontWaitFor(this.s.notifyIfUpdated(t))},this.s=new r(t)}},t.responsesAreSame=e,t}({},workbox.core._private,workbox.core._private,workbox.core._private);
|
||||
//# sourceMappingURL=workbox-broadcast-update.prod.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-broadcast-update.prod.js","sources":["../_version.js","../responsesAreSame.js","../utils/constants.js","../BroadcastCacheUpdate.js","../BroadcastUpdatePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:broadcast-update:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * Given two `Response's`, compares several header values to see if they are\n * the same or not.\n *\n * @param {Response} firstResponse\n * @param {Response} secondResponse\n * @param {Array<string>} headersToCheck\n * @return {boolean}\n *\n * @memberof module:workbox-broadcast-update\n */\nconst responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {\n if (process.env.NODE_ENV !== 'production') {\n if (!(firstResponse instanceof Response &&\n secondResponse instanceof Response)) {\n throw new WorkboxError('invalid-responses-are-same-args');\n }\n }\n const atLeastOneHeaderAvailable = headersToCheck.some((header) => {\n return firstResponse.headers.has(header) &&\n secondResponse.headers.has(header);\n });\n if (!atLeastOneHeaderAvailable) {\n if (process.env.NODE_ENV !== 'production') {\n logger.warn(`Unable to determine where the response has been updated ` +\n `because none of the headers that would be checked are present.`);\n logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);\n }\n // Just return true, indicating the that responses are the same, since we\n // can't determine otherwise.\n return true;\n }\n return headersToCheck.every((header) => {\n const headerStateComparison = firstResponse.headers.has(header) ===\n secondResponse.headers.has(header);\n const headerValueComparison = firstResponse.headers.get(header) ===\n secondResponse.headers.get(header);\n return headerStateComparison && headerValueComparison;\n });\n};\nexport { responsesAreSame };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nexport const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';\nexport const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';\nexport const DEFAULT_HEADERS_TO_CHECK = [\n 'content-length',\n 'etag',\n 'last-modified',\n];\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { timeout } from 'workbox-core/_private/timeout.js';\nimport { resultingClientExists } from 'workbox-core/_private/resultingClientExists.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { responsesAreSame } from './responsesAreSame.js';\nimport { CACHE_UPDATED_MESSAGE_TYPE, CACHE_UPDATED_MESSAGE_META, DEFAULT_HEADERS_TO_CHECK } from './utils/constants.js';\nimport './_version.js';\n// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser\n// TODO(philipwalton): remove once this Safari bug fix has been released.\n// https://bugs.webkit.org/show_bug.cgi?id=201169\nconst isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n/**\n * Generates the default payload used in update messages. By default the\n * payload includes the `cacheName` and `updatedURL` fields.\n *\n * @return Object\n * @private\n */\nfunction defaultPayloadGenerator(data) {\n return {\n cacheName: data.cacheName,\n updatedURL: data.request.url,\n };\n}\n/**\n * Uses the `postMessage()` API to inform any open windows/tabs when a cached\n * response has been updated.\n *\n * For efficiency's sake, the underlying response bodies are not compared;\n * only specific response headers are checked.\n *\n * @memberof module:workbox-broadcast-update\n */\nclass BroadcastCacheUpdate {\n /**\n * Construct a BroadcastCacheUpdate instance with a specific `channelName` to\n * broadcast messages on\n *\n * @param {Object} options\n * @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]\n * A list of headers that will be used to determine whether the responses\n * differ.\n * @param {string} [options.generatePayload] A function whose return value\n * will be used as the `payload` field in any cache update messLine truncated
|
||||
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.cacheableResponse = (function (exports, assert_js, WorkboxError_js, getFriendlyURL_js, logger_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:cacheable-response:5.1.4'] && _();
|
||||
} catch (e) {}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
/**
|
||||
* This class allows you to set up rules determining what
|
||||
* status codes and/or headers need to be present in order for a
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* to be considered cacheable.
|
||||
*
|
||||
* @memberof module:workbox-cacheable-response
|
||||
*/
|
||||
|
||||
class CacheableResponse {
|
||||
/**
|
||||
* To construct a new CacheableResponse instance you must provide at least
|
||||
* one of the `config` properties.
|
||||
*
|
||||
* If both `statuses` and `headers` are specified, then both conditions must
|
||||
* be met for the `Response` to be considered cacheable.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||
* `Response` can have and be considered cacheable.
|
||||
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||
* and expected values that a `Response` can have and be considered cacheable.
|
||||
* If multiple headers are provided, only one needs to be present.
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
{
|
||||
if (!(config.statuses || config.headers)) {
|
||||
throw new WorkboxError_js.WorkboxError('statuses-or-headers-required', {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.statuses) {
|
||||
assert_js.assert.isArray(config.statuses, {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.statuses'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
assert_js.assert.isType(config.headers, 'object', {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.headers'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._statuses = config.statuses;
|
||||
this._headers = config.headers;
|
||||
}
|
||||
/**
|
||||
* Checks a response to see whether it's cacheable or not, based on this
|
||||
* object's configuration.
|
||||
*
|
||||
* @param {Response} response The response whose cacheability is being
|
||||
* checked.
|
||||
* @return {boolean} `true` if the `Response` is cacheable, and `false`
|
||||
* otherwise.
|
||||
*/
|
||||
|
||||
|
||||
isResponseCacheable(response) {
|
||||
{
|
||||
assert_js.assert.isInstance(response, Response, {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'isResponseCacheable',
|
||||
paramName: 'response'
|
||||
});
|
||||
}
|
||||
|
||||
let cacheable = true;
|
||||
|
||||
if (this._statuses) {
|
||||
cacheable = this._statuses.includes(response.status);
|
||||
}
|
||||
|
||||
if (this._headers && cacheable) {
|
||||
cacheable = Object.keys(this._headers).some(headerName => {
|
||||
return response.headers.get(headerName) === this._headers[headerName];
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
if (!cacheable) {
|
||||
logger_js.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_js.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`);
|
||||
logger_js.logger.groupCollapsed(`View cacheability criteria here.`);
|
||||
logger_js.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses));
|
||||
logger_js.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2));
|
||||
logger_js.logger.groupEnd();
|
||||
const logFriendlyHeaders = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
logFriendlyHeaders[key] = value;
|
||||
});
|
||||
logger_js.logger.groupCollapsed(`View response status and headers here.`);
|
||||
logger_js.logger.log(`Response status: ` + response.status);
|
||||
logger_js.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2));
|
||||
logger_js.logger.groupEnd();
|
||||
logger_js.logger.groupCollapsed(`View full response details here.`);
|
||||
logger_js.logger.log(response.headers);
|
||||
logger_js.logger.log(response);
|
||||
logger_js.logger.groupEnd();
|
||||
logger_js.logger.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return cacheable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Use of this source code is governed by an MIT-style
|
||||
license that can be found in the LICENSE file or at
|
||||
https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
/**
|
||||
* A class implementing the `cacheWillUpdate` lifecycle callback. This makes it
|
||||
* easier to add in cacheability checks to requests made via Workbox's built-in
|
||||
* strategies.
|
||||
*
|
||||
* @memberof module:workbox-cacheable-response
|
||||
*/
|
||||
|
||||
class CacheableResponsePlugin {
|
||||
/**
|
||||
* To construct a new CacheableResponsePlugin instance you must provide at
|
||||
* least one of the `config` properties.
|
||||
*
|
||||
* If both `statuses` and `headers` are specified, then both conditions must
|
||||
* be met for the `Response` to be considered cacheable.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||
* `Response` can have and be considered cacheable.
|
||||
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||
* and expected values that a `Response` can have and be considered cacheable.
|
||||
* If multiple headers are provided, only one needs to be present.
|
||||
*/
|
||||
constructor(config) {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Response} options.response
|
||||
* @return {Response|null}
|
||||
* @private
|
||||
*/
|
||||
this.cacheWillUpdate = async ({
|
||||
response
|
||||
}) => {
|
||||
if (this._cacheableResponse.isResponseCacheable(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
this._cacheableResponse = new CacheableResponse(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.CacheableResponse = CacheableResponse;
|
||||
exports.CacheableResponsePlugin = CacheableResponsePlugin;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-cacheable-response.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-cacheable-response.dev.js","sources":["../_version.js","../CacheableResponse.js","../CacheableResponsePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:cacheable-response:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * This class allows you to set up rules determining what\n * status codes and/or headers need to be present in order for a\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * to be considered cacheable.\n *\n * @memberof module:workbox-cacheable-response\n */\nclass CacheableResponse {\n /**\n * To construct a new CacheableResponse instance you must provide at least\n * one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array<number>} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object<string,string>} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config = {}) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(config.statuses || config.headers)) {\n throw new WorkboxError('statuses-or-headers-required', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n });\n }\n if (config.statuses) {\n assert.isArray(config.statuses, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.statuses',\n });\n }\n if (config.headers) {\n assert.isType(config.headers, 'object', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.headers',\n });\n }\n }\n this._statuses = config.statuses;\n this._headers = config.headers;\n }\n /**\n * Checks a response to see whether it's cacheable or not, based on this\n * object's configuration.\n *\n * @param {Response} response The response whose cacheability is being\n * checked.\n * @return {boolean} `true` if the `Response` is cacheable, and `false`\n * otherwise.\n */\n isResponseCacheable(response) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(response, Response, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'isResponseCacheable',\n paramName: 'response',\n });\n }\n let cacheable = true;\n if (this._statuses) {\n cacheable = this._statuses.includes(response.status);\n }\n if (this._headers && cacheable) {\n cacheable = Object.keys(this._headers).some((headerName) => {\n return response.headers.get(headerName) === this._headers[headerName];\n });\n }\n if (process.env.NODE_ENV !== 'production') {\n if (!cacheable) {\n logger.groupCollapsed(`The request for ` +\n `'${getFriendlyURL(response.url)}' returned a response that does ` +\n `not meet the criteria for being cached.`);\n logger.groupCollapsed(`View cacheability criteria here.`);\n logger.log(`Cacheable statuses: ` +\n JSON.stringify(this._statuses));\n logger.log(`Cacheable headers: ` +\n JSON.stringify(this._headers, null, 2));\n logger.groupEnd();\n const logFriendlyHeaders = {};\n response.headers.forEach((value, key) => {\n logFriendlyHeaders[key] = value;\n });\n logger.groupCollapsed(`View response status and headers here.`);\n logger.log(`Response status: ` + responsLine truncated
|
||||
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user