diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUpload.css b/extensions/cornerstone/src/components/DicomUpload/DicomUpload.css new file mode 100644 index 000000000..55ddf7922 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUpload.css @@ -0,0 +1,6 @@ +.dicom-upload-drop-area-border-dash { + background-image: repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%); + background-position: left top, left bottom, left top, right top; + background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; + background-size: 20px 3px, 20px 3px, 3px 20px, 3px 20px; +} diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUpload.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUpload.tsx new file mode 100644 index 000000000..fa29e65d1 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUpload.tsx @@ -0,0 +1,116 @@ +import React, { useCallback, useState } from 'react'; +import { ReactElement } from 'react'; +import Dropzone from 'react-dropzone'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import DicomFileUploader from '../../utils/DicomFileUploader'; +import DicomUploadProgress from './DicomUploadProgress'; +import { Button } from '@ohif/ui'; +import './DicomUpload.css'; + +type DicomUploadProps = { + dataSource; + onComplete: () => void; + onStarted: () => void; +}; + +function DicomUpload({ + dataSource, + onComplete, + onStarted, +}: DicomUploadProps): ReactElement { + const baseClassNames = 'min-h-[520px] flex flex-col bg-black select-none'; + const [dicomFileUploaderArr, setDicomFileUploaderArr] = useState([]); + + const onDrop = useCallback(async acceptedFiles => { + onStarted(); + setDicomFileUploaderArr( + acceptedFiles.map(file => new DicomFileUploader(file, dataSource)) + ); + }, []); + + const getDropZoneComponent = (): ReactElement => { + return ( + { + onDrop(acceptedFiles); + }} + noClick + > + {({ getRootProps }) => ( +
+
+ + {({ getRootProps, getInputProps }) => ( +
+ +
+ )} +
+ + {({ getRootProps, getInputProps }) => ( +
+ +
+ )} +
+
+
or drag images or folders here
+
+ (DICOM files supported) +
+
+ )} +
+ ); + }; + + return ( + <> + {dicomFileUploaderArr.length ? ( +
+ +
+ ) : ( +
+ {getDropZoneComponent()} +
+ )} + + ); +} + +DicomUpload.propTypes = { + dataSource: PropTypes.object.isRequired, + onComplete: PropTypes.func.isRequired, + onStarted: PropTypes.func.isRequired, +}; + +export default DicomUpload; diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx new file mode 100644 index 000000000..f1b2de731 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -0,0 +1,436 @@ +import React, { + useCallback, + useEffect, + useRef, + useState, + ReactElement, +} from 'react'; +import PropTypes from 'prop-types'; +import { Button, Icon, ProgressLoadingBar } from '@ohif/ui'; +import DicomFileUploader, { + EVENTS, + UploadStatus, + DicomFileUploaderProgressEvent, + UploadRejection, +} from '../../utils/DicomFileUploader'; +import DicomUploadProgressItem from './DicomUploadProgressItem'; +import classNames from 'classnames'; + +type DicomUploadProgressProps = { + dicomFileUploaderArr: DicomFileUploader[]; + onComplete: () => void; +}; + +const ONE_SECOND = 1000; +const ONE_MINUTE = ONE_SECOND * 60; +const ONE_HOUR = ONE_MINUTE * 60; + +// The base/initial interval time length used to calculate the +// rate of the upload and in turn estimate the +// the amount of time remaining for the upload. This is the length +// of the very first interval to get a reasonable estimate on screen in +// a reasonable amount of time. The length of each interval after the first +// is based on the upload rate calculated. Faster rates use this base interval +// length. Slower rates below UPLOAD_RATE_THRESHOLD get longer interval times +// to obtain more accurate upload rates. +const BASE_INTERVAL_TIME = 15000; + +// The upload rate threshold to determine the length of the interval to +// calculate the upload rate. +const UPLOAD_RATE_THRESHOLD = 75; + +const NO_WRAP_ELLIPSIS_CLASS_NAMES = + 'text-ellipsis whitespace-nowrap overflow-hidden'; + +function DicomUploadProgress({ + dicomFileUploaderArr, + onComplete, +}: DicomUploadProgressProps): ReactElement { + const [totalUploadSize] = useState( + dicomFileUploaderArr.reduce( + (acc, fileUploader) => acc + fileUploader.getFileSize(), + 0 + ) + ); + + const currentUploadSizeRef = useRef(0); + + const uploadRateRef = useRef(0); + + const [timeRemaining, setTimeRemaining] = useState(null); + + const [percentComplete, setPercentComplete] = useState(0); + + const [numFilesCompleted, setNumFilesCompleted] = useState(0); + + const [numFails, setNumFails] = useState(0); + + const [showFailedOnly, setShowFailedOnly] = useState(false); + + const progressBarContainerRef = useRef(); + + /** + * The effect for measuring and setting the current upload rate. This is + * done by measuring the amount of data uploaded in a set interval time. + */ + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + // The amount of data already uploaded at the start of the interval. + let intervalStartUploadSize = 0; + + // The starting time of the interval. + let intervalStartTime = Date.now(); + + const setUploadRateRef = () => { + const uploadSizeFromStartOfInterval = + currentUploadSizeRef.current - intervalStartUploadSize; + + const now = Date.now(); + const timeSinceStartOfInterval = now - intervalStartTime; + + // Calculate and set the upload rate (ref) + uploadRateRef.current = + uploadSizeFromStartOfInterval / timeSinceStartOfInterval; + + // Reset the interval starting values. + intervalStartUploadSize = currentUploadSizeRef.current; + intervalStartTime = now; + + // Only start a new interval if there is more to upload. + if (totalUploadSize - currentUploadSizeRef.current > 0) { + if (uploadRateRef.current >= UPLOAD_RATE_THRESHOLD) { + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME); + } else { + // The current upload rate is relatively slow, so use a larger + // time interval to get a better upload rate estimate. + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME * 2); + } + } + }; + + // The very first interval is just the base time interval length. + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME); + + return () => { + clearTimeout(timeoutId); + }; + }, []); + + /** + * The effect for: updating the overall percentage complete; setting the + * estimated time remaining; updating the number of files uploaded; and + * detecting if any error has occurred. + */ + useEffect(() => { + let currentTimeRemaining = null; + + // For each uploader, listen for the progress percentage complete and + // add promise catch/finally callbacks to detect errors and count number + // of uploads complete. + const subscriptions = dicomFileUploaderArr.map(fileUploader => { + let currentFileUploadSize = 0; + + const updateProgress = (percentComplete: number) => { + const previousFileUploadSize = currentFileUploadSize; + + currentFileUploadSize = Math.round( + (percentComplete / 100) * fileUploader.getFileSize() + ); + + currentUploadSizeRef.current = Math.min( + totalUploadSize, + currentUploadSizeRef.current - + previousFileUploadSize + + currentFileUploadSize + ); + + setPercentComplete( + (currentUploadSizeRef.current / totalUploadSize) * 100 + ); + + if (uploadRateRef.current !== 0) { + const uploadSizeRemaining = + totalUploadSize - currentUploadSizeRef.current; + + const timeRemaining = Math.round( + uploadSizeRemaining / uploadRateRef.current + ); + + if (currentTimeRemaining === null) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + return; + } + + // Do not show an increase in the time remaining by two seconds or minutes + // so as to prevent jumping the time remaining up and down constantly + // due to rounding, inaccuracies in the estimate and slight variations + // in upload rates over time. + if (timeRemaining < ONE_MINUTE) { + const currentSecondsRemaining = Math.ceil( + currentTimeRemaining / ONE_SECOND + ); + const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND); + const delta = secondsRemaining - currentSecondsRemaining; + if (delta < 0 || delta > 2) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + return; + } + + if (timeRemaining < ONE_HOUR) { + const currentMinutesRemaining = Math.ceil( + currentTimeRemaining / ONE_MINUTE + ); + const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE); + const delta = minutesRemaining - currentMinutesRemaining; + if (delta < 0 || delta > 2) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + return; + } + + // Hours remaining... + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + }; + + const progressCallback = ( + progressEvent: DicomFileUploaderProgressEvent + ) => { + updateProgress(progressEvent.percentComplete); + }; + + // Use the uploader promise to flag any error and count the number of + // uploads completed. + fileUploader + .load() + .catch((rejection: UploadRejection) => { + if (rejection.status === UploadStatus.Failed) { + setNumFails(numFails => numFails + 1); + } + }) + .finally(() => { + // If any error occurred, the percent complete progress stops firing + // but this call to updateProgress nicely puts all finished uploads at 100%. + updateProgress(100); + setNumFilesCompleted(numCompleted => numCompleted + 1); + }); + + return fileUploader.subscribe(EVENTS.PROGRESS, progressCallback); + }); + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + }; + }, []); + + const cancelAllUploads = useCallback(async () => { + for (const dicomFileUploader of dicomFileUploaderArr) { + // Important: we need a non-blocking way to cancel every upload, + // otherwise the UI will freeze and the user will not be able + // to interact with the app and progress will not be updated. + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + dicomFileUploader.cancel(); + resolve(); + }, 0); + }); + } + }, []); + + const getFormattedTimeRemaining = useCallback((): string => { + if (timeRemaining == null) { + return ''; + } + + if (timeRemaining < ONE_MINUTE) { + const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND); + return `${secondsRemaining} ${ + secondsRemaining === 1 ? 'second' : 'seconds' + }`; + } + + if (timeRemaining < ONE_HOUR) { + const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE); + return `${minutesRemaining} ${ + minutesRemaining === 1 ? 'minute' : 'minutes' + }`; + } + + const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR); + return `${hoursRemaining} ${hoursRemaining === 1 ? 'hour' : 'hours'}`; + }, [timeRemaining]); + + const getPercentCompleteRounded = useCallback( + () => Math.min(100, Math.round(percentComplete)), + [percentComplete] + ); + + /** + * Determines if the progress bar should show the infinite animation or not. + * Show the infinite animation for progress less than 1% AND if less than + * one pixel of the progress bar would be displayed. + */ + const showInfiniteProgressBar = useCallback((): boolean => { + return ( + getPercentCompleteRounded() < 1 && + (progressBarContainerRef?.current?.offsetWidth ?? 0) * + (percentComplete / 100) < + 1 + ); + }, [getPercentCompleteRounded, percentComplete]); + + /** + * Gets the css style for the 'n of m' (files completed) text. The only css attribute + * of the style is width such that the 'n of m' is always a fixed width and thus + * as each file completes uploading the text on screen does not constantly shift + * left and right. + */ + const getNofMFilesStyle = useCallback(() => { + // the number of digits accounts for the digits being on each side of the ' of ' + const numDigits = 2 * dicomFileUploaderArr.length.toString().length; + // the number of digits + 2 spaces and 2 characters for ' of ' + const numChars = numDigits + 4; + return { width: `${numChars}ch` }; + }, []); + + const getNumCompletedAndTimeRemainingComponent = (): ReactElement => { + return ( +
+ {numFilesCompleted === dicomFileUploaderArr.length ? ( + <> + {`${ + dicomFileUploaderArr.length + } ${ + dicomFileUploaderArr.length > 1 ? 'files' : 'file' + } completed.`} + + + ) : ( + <> + + {`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}  + + + {' files completed.'}  + + + {timeRemaining + ? `Less than ${getFormattedTimeRemaining()} remaining. ` + : ''} + + + Cancel All Uploads + + + )} +
+ ); + }; + + const getShowFailedOnlyIconComponent = (): ReactElement => { + return ( +
+ {numFails > 0 && ( +
+ setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly) + } + > + +
+ )} +
+ ); + }; + + const getPercentCompleteComponent = (): ReactElement => { + return ( +
+
+ {numFilesCompleted === dicomFileUploaderArr.length ? ( + <> +
+ {numFails > 0 + ? `Completed with ${numFails} ${ + numFails > 1 ? 'errors' : 'error' + }!` + : 'Completed!'} +
+ {getShowFailedOnlyIconComponent()} + + ) : ( + <> +
+ +
+
+
{`${getPercentCompleteRounded()}%`}
+ {getShowFailedOnlyIconComponent()} +
+ + )} +
+
+ ); + }; + + return ( +
+ {getNumCompletedAndTimeRemainingComponent()} +
+ {getPercentCompleteComponent()} +
+ {dicomFileUploaderArr + .filter( + dicomFileUploader => + !showFailedOnly || + dicomFileUploader.getStatus() === UploadStatus.Failed + ) + .map(dicomFileUploader => ( + + ))} +
+
+
+ ); +} + +DicomUploadProgress.propTypes = { + dicomFileUploaderArr: PropTypes.arrayOf( + PropTypes.instanceOf(DicomFileUploader) + ).isRequired, + onComplete: PropTypes.func.isRequired, +}; + +export default DicomUploadProgress; diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx new file mode 100644 index 000000000..bcbdcec58 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx @@ -0,0 +1,117 @@ +import React, { + ReactElement, + memo, + useCallback, + useEffect, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import DicomFileUploader, { + DicomFileUploaderProgressEvent, + EVENTS, + UploadRejection, + UploadStatus, +} from '../../utils/DicomFileUploader'; +import { Icon } from '@ohif/ui'; + +type DicomUploadProgressItemProps = { + dicomFileUploader: DicomFileUploader; +}; + +// eslint-disable-next-line react/display-name +const DicomUploadProgressItem = memo( + ({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => { + const [percentComplete, setPercentComplete] = useState( + dicomFileUploader.getPercentComplete() + ); + const [failedReason, setFailedReason] = useState(''); + const [status, setStatus] = useState(dicomFileUploader.getStatus()); + + console.info(`${dicomFileUploader.getFileId()}`); + const isComplete = useCallback(() => { + return ( + status === UploadStatus.Failed || + status === UploadStatus.Cancelled || + status === UploadStatus.Success + ); + }, [status]); + + useEffect(() => { + const progressSubscription = dicomFileUploader.subscribe( + EVENTS.PROGRESS, + (dicomFileUploaderProgressEvent: DicomFileUploaderProgressEvent) => { + setPercentComplete(dicomFileUploaderProgressEvent.percentComplete); + } + ); + + dicomFileUploader + .load() + .catch((reason: UploadRejection) => { + setStatus(reason.status); + setFailedReason(reason.message ?? ''); + }) + .finally(() => setStatus(dicomFileUploader.getStatus())); + + return () => progressSubscription.unsubscribe(); + }, []); + + const cancelUpload = useCallback(() => { + dicomFileUploader.cancel(); + }, []); + + const getStatusIcon = (): ReactElement => { + switch (dicomFileUploader.getStatus()) { + case UploadStatus.Success: + return ( + + ); + case UploadStatus.InProgress: + return ; + case UploadStatus.Failed: + return ; + case UploadStatus.Cancelled: + return ; + default: + return <>; + } + }; + + return ( +
+
+
+
+ {getStatusIcon()} +
+
+ {dicomFileUploader.getFileName()} +
+
+ {failedReason &&
{failedReason}
} +
+
+ {!isComplete() && ( + <> + {dicomFileUploader.getStatus() === UploadStatus.InProgress && ( +
{percentComplete}%
+ )} +
+ +
+ + )} +
+
+ ); + } +); + +DicomUploadProgressItem.propTypes = { + dicomFileUploader: PropTypes.instanceOf(DicomFileUploader).isRequired, +}; + +export default DicomUploadProgressItem; diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts index fa548b74a..acb064244 100644 --- a/extensions/cornerstone/src/getCustomizationModule.ts +++ b/extensions/cornerstone/src/getCustomizationModule.ts @@ -1,5 +1,6 @@ import { Enums } from '@cornerstonejs/tools'; import { toolNames } from './initCornerstoneTools'; +import DicomUpload from './components/DicomUpload/DicomUpload'; const tools = { active: [ @@ -22,6 +23,13 @@ const tools = { function getCustomizationModule() { return [ + { + name: 'cornerstoneDicomUploadComponent', + value: { + id: 'dicomUploadComponent', + component: DicomUpload, + }, + }, { name: 'default', value: [ diff --git a/extensions/cornerstone/src/utils/DicomFileUploader.ts b/extensions/cornerstone/src/utils/DicomFileUploader.ts new file mode 100644 index 000000000..64465e597 --- /dev/null +++ b/extensions/cornerstone/src/utils/DicomFileUploader.ts @@ -0,0 +1,220 @@ +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; + +import { PubSubService } from '@ohif/core'; + +export const EVENTS = { + PROGRESS: 'event:DicomFileUploader:progress', +}; + +export interface DicomFileUploaderEvent { + fileId: number; +} + +export interface DicomFileUploaderProgressEvent extends DicomFileUploaderEvent { + percentComplete: number; +} + +export enum UploadStatus { + NotStarted, + InProgress, + Success, + Failed, + Cancelled, +} + +type CancelOrFailed = UploadStatus.Cancelled | UploadStatus.Failed; + +export class UploadRejection { + message: string; + status: CancelOrFailed; + + constructor(status: CancelOrFailed, message: string) { + this.message = message; + this.status = status; + } +} + +export default class DicomFileUploader extends PubSubService { + private _file; + private _fileId; + private _dataSource; + private _loadPromise; + private _abortController = new AbortController(); + private _status: UploadStatus = UploadStatus.NotStarted; + private _percentComplete = 0; + + constructor(file, dataSource) { + super(EVENTS); + this._file = file; + this._fileId = dicomImageLoader.wadouri.fileManager.add(file); + this._dataSource = dataSource; + } + + getFileId(): string { + return this._fileId; + } + + getFileName(): string { + return this._file.name; + } + + getFileSize(): number { + return this._file.size; + } + + cancel(): void { + this._abortController.abort(); + } + + getStatus(): UploadStatus { + return this._status; + } + + getPercentComplete(): number { + return this._percentComplete; + } + + async load(): Promise { + if (this._loadPromise) { + // Already started loading, return the load promise. + return this._loadPromise; + } + + this._loadPromise = new Promise((resolve, reject) => { + // The upload listeners: fire progress events and/or settle the promise. + const uploadCallbacks = { + progress: evt => { + if (!evt.lengthComputable) { + // Progress computation is not possible. + return; + } + + this._status = UploadStatus.InProgress; + + this._percentComplete = Math.round((100 * evt.loaded) / evt.total); + this._broadcastEvent(EVENTS.PROGRESS, { + fileId: this._fileId, + percentComplete: this._percentComplete, + }); + }, + timeout: () => { + this._reject( + reject, + new UploadRejection(UploadStatus.Failed, 'The request timed out.') + ); + }, + abort: () => { + this._reject( + reject, + new UploadRejection(UploadStatus.Cancelled, 'Cancelled') + ); + }, + error: () => { + this._reject( + reject, + new UploadRejection(UploadStatus.Failed, 'The request failed.') + ); + }, + }; + + // First try to load the file. + dicomImageLoader.wadouri + .loadFileRequest(this._fileId) + .then(dicomFile => { + if (this._abortController.signal.aborted) { + this._reject( + reject, + new UploadRejection(UploadStatus.Cancelled, 'Cancelled') + ); + return; + } + + if (!this._checkDicomFile(dicomFile)) { + // The file is not DICOM + this._reject( + reject, + new UploadRejection( + UploadStatus.Failed, + 'Not a valid DICOM file.' + ) + ); + return; + } + + const request = new XMLHttpRequest(); + this._addRequestCallbacks(request, uploadCallbacks); + + // Do the actual upload by supplying the DICOM file and upload callbacks/listeners. + return this._dataSource.store + .dicom(dicomFile, request) + .then(() => { + this._status = UploadStatus.Success; + resolve(); + }) + .catch(reason => { + this._reject(reject, reason); + }); + }) + .catch(reason => { + this._reject(reject, reason); + }); + }); + + return this._loadPromise; + } + + private _isRejected(): boolean { + return ( + this._status === UploadStatus.Failed || + this._status === UploadStatus.Cancelled + ); + } + + private _reject(reject: (reason?: any) => void, reason: any) { + if (this._isRejected()) { + return; + } + + if (reason instanceof UploadRejection) { + this._status = reason.status; + reject(reason); + return; + } + + this._status = UploadStatus.Failed; + + if (reason.message) { + reject(new UploadRejection(UploadStatus.Failed, reason.message)); + return; + } + + reject(new UploadRejection(UploadStatus.Failed, reason)); + } + + private _addRequestCallbacks(request: XMLHttpRequest, uploadCallbacks) { + const abortCallback = () => request.abort(); + this._abortController.signal.addEventListener('abort', abortCallback); + + for (const [eventName, callback] of Object.entries(uploadCallbacks)) { + request.upload.addEventListener(eventName, callback); + } + + const cleanUpCallback = () => { + this._abortController.signal.removeEventListener('abort', abortCallback); + + for (const [eventName, callback] of Object.entries(uploadCallbacks)) { + request.upload.removeEventListener(eventName, callback); + } + + request.removeEventListener('loadend', cleanUpCallback); + }; + request.addEventListener('loadend', cleanUpCallback); + } + + private _checkDicomFile(arrayBuffer: ArrayBuffer) { + if (arrayBuffer.length <= 132) return false; + const arr = new Uint8Array(arrayBuffer.slice(128, 132)); + // bytes from 128 to 132 must be "DICM" + return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]); + } +} diff --git a/extensions/default/package.json b/extensions/default/package.json index 81ca66393..958ebb288 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -33,7 +33,7 @@ "@ohif/core": "^3.0.0", "@ohif/i18n": "^1.0.0", "dcmjs": "^0.29.5", - "dicomweb-client": "^0.8.4", + "dicomweb-client": "^0.10.2", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 792ee44c7..c5782375c 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -61,6 +61,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { singlepart, } = dicomWebConfig; + const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); + const qidoConfig = { url: qidoRoot, staticWado, @@ -230,36 +232,47 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }, store: { - dicom: async dataset => { + dicom: async (dataset, request) => { const headers = userAuthenticationService.getAuthorizationHeader(); if (headers) { wadoDicomWebClient.headers = headers; } - const meta = { - FileMetaInformationVersion: - dataset._meta.FileMetaInformationVersion.Value, - MediaStorageSOPClassUID: dataset.SOPClassUID, - MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, - ImplementationClassUID, - ImplementationVersionName, - }; + if (dataset instanceof ArrayBuffer) { + const options = { + datasets: [dataset], + request, + }; - const denaturalized = denaturalizeDataset(meta); - const dicomDict = new DicomDict(denaturalized); + await wadoDicomWebClient.storeInstances(options); + } else { + const meta = { + FileMetaInformationVersion: + dataset._meta.FileMetaInformationVersion.Value, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, + }; - dicomDict.dict = denaturalizeDataset(dataset); + const denaturalized = denaturalizeDataset(meta); + const dicomDict = new DicomDict(denaturalized); - const part10Buffer = dicomDict.write(); + dicomDict.dict = denaturalizeDataset(dataset); - const options = { - datasets: [part10Buffer], - }; + const part10Buffer = dicomDict.write(); - await wadoDicomWebClient.storeInstances(options); + const options = { + datasets: [part10Buffer], + request, + }; + + await wadoDicomWebClient.storeInstances(options); + } }, }, + _retrieveSeriesMetadataSync: async ( StudyInstanceUID, filters, @@ -482,6 +495,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }); return imageIds; }, + getConfig() { + return dicomWebConfigCopy; + }, }; if (supportsReject) { diff --git a/platform/core/package.json b/platform/core/package.json index 1bfa7985f..43d410446 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -43,7 +43,7 @@ "dependencies": { "@babel/runtime": "^7.20.13", "dcmjs": "^0.29.5", - "dicomweb-client": "^0.8.4", + "dicomweb-client": "^0.10.2", "isomorphic-base64": "^1.0.2", "lodash.merge": "^4.6.1", "lodash.clonedeep": "^4.5.0", diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index 2cf785be3..a0b1a713f 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -21,6 +21,7 @@ function create({ deleteStudyMetadataPromise, getImageIdsForDisplaySet, getImageIdsForInstance, + getConfig, }) { const defaultQuery = { studies: { @@ -57,6 +58,11 @@ function create({ }; const defaultReject = {}; + + const defaultGetConfig = () => { + return { dicomUploadEnabled: false }; + }; + return { query: query || defaultQuery, retrieve: retrieve || defaultRetrieve, @@ -66,6 +72,7 @@ function create({ deleteStudyMetadataPromise, getImageIdsForDisplaySet, getImageIdsForInstance, + getConfig: getConfig || defaultGetConfig, }; } diff --git a/platform/ui/src/assets/icons/icon-alert-small.svg b/platform/ui/src/assets/icons/icon-alert-small.svg new file mode 100644 index 000000000..51151d709 --- /dev/null +++ b/platform/ui/src/assets/icons/icon-alert-small.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/platform/ui/src/assets/icons/icon-status-alert.svg b/platform/ui/src/assets/icons/icon-status-alert.svg new file mode 100644 index 000000000..615d4a2fd --- /dev/null +++ b/platform/ui/src/assets/icons/icon-status-alert.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/platform/ui/src/assets/icons/icon-transferring.svg b/platform/ui/src/assets/icons/icon-transferring.svg new file mode 100644 index 000000000..86379e138 --- /dev/null +++ b/platform/ui/src/assets/icons/icon-transferring.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/icon-upload.svg b/platform/ui/src/assets/icons/icon-upload.svg new file mode 100644 index 000000000..cdd1366fa --- /dev/null +++ b/platform/ui/src/assets/icons/icon-upload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/icons-alert-outline.svg b/platform/ui/src/assets/icons/icons-alert-outline.svg new file mode 100644 index 000000000..747b65c78 --- /dev/null +++ b/platform/ui/src/assets/icons/icons-alert-outline.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/platform/ui/src/components/Button/Button.tsx b/platform/ui/src/components/Button/Button.tsx index 939d803fd..8e8fcf7a2 100644 --- a/platform/ui/src/components/Button/Button.tsx +++ b/platform/ui/src/components/Button/Button.tsx @@ -58,6 +58,8 @@ const variants = { contained: { default: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80', primary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + primaryDark: + 'text-primary-active hover:opacity-80 active:opacity-100 focus:opacity-80', secondary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80', white: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80', @@ -93,6 +95,7 @@ const defaultVariantBackGroundColor = { contained: { default: 'bg-primary-light', primary: 'bg-primary-main', + primaryDark: 'bg-primary-dark', secondary: 'bg-secondary-light', white: 'bg-white', black: 'bg-black', @@ -231,6 +234,7 @@ Button.propTypes = { color: PropTypes.oneOf([ 'default', 'primary', + 'primaryDark', 'primaryActive', 'secondary', 'white', diff --git a/platform/ui/src/components/Icon/getIcon.js b/platform/ui/src/components/Icon/getIcon.js index 3627697af..103bcef9e 100644 --- a/platform/ui/src/components/Icon/getIcon.js +++ b/platform/ui/src/components/Icon/getIcon.js @@ -50,6 +50,8 @@ import tracked from './../../assets/icons/tracked.svg'; import unlink from './../../assets/icons/unlink.svg'; import checkboxChecked from './../../assets/icons/checkbox-checked.svg'; import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg'; +import iconAlertOutline from './../../assets/icons/icons-alert-outline.svg'; +import iconAlertSmall from './../../assets/icons/icon-alert-small.svg'; import iconClose from './../../assets/icons/icon-close.svg'; import iconNextInactive from './../../assets/icons/icon-next-inactive.svg'; import iconNext from './../../assets/icons/icon-next.svg'; @@ -57,6 +59,9 @@ import iconPlay from './../../assets/icons/icon-play.svg'; import iconPause from './../../assets/icons/icon-pause.svg'; import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg'; import iconPrev from './../../assets/icons/icon-prev.svg'; +import iconStatusAlert from './../../assets/icons/icon-status-alert.svg'; +import iconTransferring from './../../assets/icons/icon-transferring.svg'; +import iconUpload from './../../assets/icons/icon-upload.svg'; import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg'; import navigationPanelRightReveal from './../../assets/icons/navigation-panel-right-reveal.svg'; import tabLinear from './../../assets/icons/tab-linear.svg'; @@ -144,9 +149,13 @@ const ICONS = { 'external-link': externalLink, 'group-layers': groupLayers, info: info, + 'icon-alert-outline': iconAlertOutline, + 'icon-alert-small': iconAlertSmall, 'icon-close': iconClose, 'icon-play': iconPlay, 'icon-pause': iconPause, + 'icon-status-alert': iconStatusAlert, + 'icon-transferring': iconTransferring, 'info-action': infoAction, 'info-link': infoLink, 'arrow-left': arrowLeft, @@ -230,6 +239,7 @@ const ICONS = { 'icon-next': iconNext, 'icon-prev-inactive': iconPrevInactive, 'icon-prev': iconPrev, + 'icon-upload': iconUpload, 'navigation-panel-right-hide': navigationPanelRightHide, 'navigation-panel-right-reveal': navigationPanelRightReveal, 'tab-linear': tabLinear, diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 78376bd83..2c18a8bf4 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,8 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import { Icon } from '@ohif/ui'; - -import './LoadingIndicatorProgress.css'; +import ProgressLoadingBar from '../ProgressLoadingBar'; /** * A React component that renders a loading indicator. @@ -19,18 +18,8 @@ function LoadingIndicatorProgress({ className, textBlock, progress }) { )} > -
- {progress === undefined || progress === null ? ( -
- ) : ( -
- )} +
+
{textBlock}
diff --git a/platform/ui/src/components/Modal/Modal.tsx b/platform/ui/src/components/Modal/Modal.tsx index 7d0d9e67e..0aa409f49 100644 --- a/platform/ui/src/components/Modal/Modal.tsx +++ b/platform/ui/src/components/Modal/Modal.tsx @@ -18,6 +18,7 @@ const Modal = ({ title, onClose, children, + shouldCloseOnOverlayClick, }) => { const { hide } = useModal(); @@ -56,6 +57,7 @@ const Modal = ({ onRequestClose={handleClose} isOpen={isOpen} title={title} + shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} > {renderHeader()}
@@ -67,6 +69,7 @@ const Modal = ({ Modal.defaultProps = { shouldCloseOnEsc: true, + shouldCloseOnOverlayClick: true, }; Modal.propTypes = { @@ -80,6 +83,7 @@ Modal.propTypes = { PropTypes.arrayOf(PropTypes.node), PropTypes.node, ]).isRequired, + shouldCloseOnOverlayClick: PropTypes.bool, }; export default Modal; diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.css b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.css similarity index 71% rename from platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.css rename to platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.css index b07886041..f9addbe9e 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.css +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.css @@ -4,7 +4,7 @@ border-radius: 4px; overflow: hidden; position: relative; - width: 12em; + width: 100%; } .infinite-loading-bar { @@ -15,14 +15,6 @@ width: 50%; } -.loading-bar { - animation: side2side 2s ease-in-out infinite; - height: 100%; - border-radius: 4px; - position: absolute; - width: 50%; -} - @keyframes side2side { 0%, 100% { diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx new file mode 100644 index 000000000..e5807b4f7 --- /dev/null +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -0,0 +1,34 @@ +import React, { ReactElement } from 'react'; + +import './ProgressLoadingBar.css'; + +export type ProgressLoadingBarProps = { + progress?: number; +}; +/** + * A React component that renders a loading progress bar. + * If progress is not provided, it will render an infinite loading bar + * If progress is provided, it will render a progress bar + * The progress text can be optionally displayed to the left of the bar. + */ +function ProgressLoadingBar({ + progress, +}: ProgressLoadingBarProps): ReactElement { + return ( +
+ {progress === undefined || progress === null ? ( +
+ ) : ( +
+ )} +
+ ); +} + +export default ProgressLoadingBar; diff --git a/platform/ui/src/components/ProgressLoadingBar/index.js b/platform/ui/src/components/ProgressLoadingBar/index.js new file mode 100644 index 000000000..ab8869ffc --- /dev/null +++ b/platform/ui/src/components/ProgressLoadingBar/index.js @@ -0,0 +1,2 @@ +import ProgressLoadingBar from './ProgressLoadingBar'; +export default ProgressLoadingBar; diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.tsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.tsx index 1e8008c80..a266c00dc 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.tsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.tsx @@ -11,6 +11,7 @@ const StudyListFilter = ({ clearFilters, isFiltering, numOfStudies, + onUploadClick, }) => { const { t } = useTranslation('StudyList'); const { sortBy, sortDirection } = filterValues; @@ -33,6 +34,15 @@ const StudyListFilter = ({ {t('StudyList')} + {onUploadClick && ( +
+ + Upload +
+ )}
{isFiltering && ( @@ -119,6 +129,7 @@ StudyListFilter.propTypes = { onChange: PropTypes.func.isRequired, clearFilters: PropTypes.func.isRequired, isFiltering: PropTypes.bool.isRequired, + onUploadClick: PropTypes.func, }; export default StudyListFilter; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index d01247542..4ca26c7e2 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -70,6 +70,7 @@ import CheckBox from './CheckBox'; import LoadingIndicatorProgress from './LoadingIndicatorProgress'; import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent'; import ViewportActionBar from './ViewportActionBar'; +import ProgressLoadingBar from './ProgressLoadingBar'; export { AboutModal, @@ -110,6 +111,7 @@ export { Modal, NavBar, Notification, + ProgressLoadingBar, Select, SegmentationTable, SegmentationGroupTable, diff --git a/platform/ui/src/contextProviders/ModalProvider.tsx b/platform/ui/src/contextProviders/ModalProvider.tsx index 382b8a242..85902df2f 100644 --- a/platform/ui/src/contextProviders/ModalProvider.tsx +++ b/platform/ui/src/contextProviders/ModalProvider.tsx @@ -32,6 +32,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { content: null, contentProps: null, shouldCloseOnEsc: true, + shouldCloseOnOverlayClick: true, isOpen: true, closeButton: true, title: null, @@ -39,7 +40,6 @@ const ModalProvider = ({ children, modal: Modal, service }) => { }; const { t } = useTranslation('Modals'); - const [options, setOptions] = useState(DEFAULT_OPTIONS); /** @@ -57,9 +57,9 @@ const ModalProvider = ({ children, modal: Modal, service }) => { * * @returns void */ - const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [ - DEFAULT_OPTIONS, - ]); + const hide = useCallback(() => { + setOptions(DEFAULT_OPTIONS); + }, [DEFAULT_OPTIONS]); /** * Sets the implementation of a modal service that can be used by extensions. @@ -80,6 +80,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { customClassName, shouldCloseOnEsc, closeButton, + shouldCloseOnOverlayClick, } = options; return ( @@ -92,6 +93,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { title={t(title)} closeButton={closeButton} onClose={hide} + shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} > diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index fad709a2c..7b51fd785 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -70,6 +70,7 @@ export { Modal, NavBar, Notification, + ProgressLoadingBar, Select, SegmentationTable, SegmentationGroupTable, diff --git a/platform/viewer/public/config/local_dcm4chee.js b/platform/viewer/public/config/local_dcm4chee.js index fca1f2a71..44dbba9d3 100644 --- a/platform/viewer/public/config/local_dcm4chee.js +++ b/platform/viewer/public/config/local_dcm4chee.js @@ -1,5 +1,9 @@ window.config = { routerBasename: '/', + customizationService: { + dicomUploadComponent: + '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', + }, showStudyList: true, extensions: [], modes: [], @@ -26,6 +30,7 @@ window.config = { requestOptions: { auth: 'admin:admin', }, + dicomUploadEnabled: true, }, }, { diff --git a/platform/viewer/public/config/local_orthanc.js b/platform/viewer/public/config/local_orthanc.js index 71bacacbe..9b5d086b6 100644 --- a/platform/viewer/public/config/local_orthanc.js +++ b/platform/viewer/public/config/local_orthanc.js @@ -3,6 +3,10 @@ window.config = { // whiteLabelling: {}, extensions: [], modes: [], + customizationService: { + dicomUploadComponent: + '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', + }, showStudyList: true, maxNumberOfWebWorkers: 3, showLoadingIndicator: true, @@ -28,6 +32,7 @@ window.config = { useBulkDataURI: false, supportsFuzzyMatching: true, supportsWildcard: true, + dicomUploadEnabled: true, }, }, { diff --git a/platform/viewer/src/routes/DataSourceWrapper.tsx b/platform/viewer/src/routes/DataSourceWrapper.tsx index 51fa05d95..d5357875e 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.tsx +++ b/platform/viewer/src/routes/DataSourceWrapper.tsx @@ -56,13 +56,14 @@ function DataSourceWrapper(props) { // But only for LayoutTemplate type of 'list'? // Or no data fetching here, and just hand down my source const STUDIES_LIMIT = 101; - const [data, setData] = useState({ + const DEFAULT_DATA = { studies: [], total: 0, resultsPerPage: 25, pageNumber: 1, location: 'Not a valid location, causes first load to occur', - }); + }; + const [data, setData] = useState(DEFAULT_DATA); const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -125,6 +126,8 @@ function DataSourceWrapper(props) { dataTotal={data.total} dataSource={dataSource} isLoadingData={isLoading} + // To refresh the data, simply reset it to DEFAULT_DATA which invalidates it and triggers a new query to fetch the data. + onRefresh={() => setData(DEFAULT_DATA)} /> ); } diff --git a/platform/viewer/src/routes/WorkList/WorkList.tsx b/platform/viewer/src/routes/WorkList/WorkList.tsx index e2e1fde2c..52d8928ca 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.tsx +++ b/platform/viewer/src/routes/WorkList/WorkList.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import filtersMeta from './filtersMeta.js'; import { useAppConfig } from '@state'; import { useDebounce, useSearchParams } from '@hooks'; -import { utils, hotkeys } from '@ohif/core'; +import { utils, hotkeys, ServicesManager } from '@ohif/core'; import { Icon, @@ -47,6 +47,8 @@ function WorkList({ dataSource, hotkeysManager, dataPath, + onRefresh, + servicesManager, }) { const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager; const { show, hide } = useModal(); @@ -429,6 +431,33 @@ function WorkList({ }); } + const { customizationService } = servicesManager.services; + const { component: dicomUploadComponent } = + customizationService.get('dicomUploadComponent') ?? {}; + const uploadProps = + dicomUploadComponent && dataSource.getConfig().dicomUploadEnabled + ? { + title: 'Upload files', + closeButton: true, + shouldCloseOnEsc: false, + shouldCloseOnOverlayClick: false, + content: dicomUploadComponent.bind(null, { + dataSource, + onComplete: () => { + hide(); + onRefresh(); + }, + onStarted: () => { + show({ + ...uploadProps, + // when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself + closeButton: false, + }); + }, + }), + } + : undefined; + return (
setFilterValues(defaultFilterValues)} isFiltering={isFiltering(filterValues, defaultFilterValues)} + onUploadClick={uploadProps ? () => show(uploadProps) : undefined} /> {hasStudies ? ( <> @@ -478,8 +508,10 @@ WorkList.propTypes = { data: PropTypes.array.isRequired, dataSource: PropTypes.shape({ query: PropTypes.object.isRequired, + getConfig: PropTypes.func, }).isRequired, isLoadingData: PropTypes.bool.isRequired, + servicesManager: PropTypes.instanceOf(ServicesManager), }; const defaultFilterValues = { diff --git a/platform/viewer/src/routes/index.tsx b/platform/viewer/src/routes/index.tsx index ab5a605a5..90162b2a4 100644 --- a/platform/viewer/src/routes/index.tsx +++ b/platform/viewer/src/routes/index.tsx @@ -25,12 +25,6 @@ const bakedInRoutes = [ // NOT FOUND (404) const notFoundRoute = { component: NotFound }; -const WorkListRoute = { - path: '/', - children: DataSourceWrapper, - private: true, - props: { children: WorkList }, -}; const createRoutes = ({ modes, @@ -54,6 +48,13 @@ const createRoutes = ({ const { customizationService } = servicesManager.services; + const WorkListRoute = { + path: '/', + children: DataSourceWrapper, + private: true, + props: { children: WorkList, servicesManager }, + }; + const customRoutes = customizationService.getGlobalCustomization( 'customRoutes' ); diff --git a/yarn.lock b/yarn.lock index e4f3f7807..d8eda8ae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1272,7 +1272,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.11" -"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@7.17.9", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.21.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -1387,34 +1387,19 @@ resolved "https://registry.npmjs.org/@cornerstonejs/calculate-suv/-/calculate-suv-1.0.3.tgz#6d99a72032c0f90cebf44dc6f0b12a5f1102e884" integrity sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w== -"@cornerstonejs/codec-charls@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-charls/-/codec-charls-0.1.1.tgz#e55d4aa908732d0cc902888b7f3856c5a996df7f" - integrity sha512-Y250DGVzmownJ7WgpHxNqWvfTnv4/malaKm/tWm0xE1FxhQE8iErMWFpKxpNDk3MdfXO4/98piVsUwmJMiWoDQ== - "@cornerstonejs/codec-charls@^1.2.3": version "1.2.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-charls/-/codec-charls-1.2.3.tgz#6952c420486822ac8404409ae0ed5a559aff6e25" + resolved "https://registry.npmjs.org/@cornerstonejs/codec-charls/-/codec-charls-1.2.3.tgz#6952c420486822ac8404409ae0ed5a559aff6e25" integrity sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA== -"@cornerstonejs/codec-libjpeg-turbo-8bit@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-0.0.7.tgz#2ea9b575eed19e6e7e3701b7a50a4ae0ffbef0c4" - integrity sha512-qgm6BuVAy5mNP8SJ+A6+VbmPnqgj8jPvJrw4HbUoAzndmf9/VHjTYwawn3kmZWya5ErFAsXQ6c0U0noB1LKAiA== - "@cornerstonejs/codec-libjpeg-turbo-8bit@^1.2.2": version "1.2.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144" + resolved "https://registry.npmjs.org/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144" integrity sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg== -"@cornerstonejs/codec-openjpeg@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b" - integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg== - "@cornerstonejs/codec-openjpeg@^1.2.2": version "1.2.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.2.tgz#f0b524235b5551426b46db197a37b06f8ac805d7" + resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.2.tgz#f0b524235b5551426b46db197a37b06f8ac805d7" integrity sha512-b1O7lZacKXelgeV9n8XWZ7pTw3i4Bq4qQ26G5ahBjWoOw4QNcCrb5hPxWBxNB/I8AoNbJxAe+lyLtyQGfdrTbw== "@cornerstonejs/codec-openjph@^2.4.2": @@ -3423,35 +3408,6 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" -"@mapbox/jsonlint-lines-primitives@~2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" - integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== - -"@mapbox/mapbox-gl-style-spec@^13.23.1": - version "13.28.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz#2ec226320a0f77856046e000df9b419303a56458" - integrity sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg== - dependencies: - "@mapbox/jsonlint-lines-primitives" "~2.0.2" - "@mapbox/point-geometry" "^0.1.0" - "@mapbox/unitbezier" "^0.0.0" - csscolorparser "~1.0.2" - json-stringify-pretty-compact "^2.0.0" - minimist "^1.2.6" - rw "^1.3.3" - sort-object "^0.3.2" - -"@mapbox/point-geometry@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" - integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== - -"@mapbox/unitbezier@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e" - integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== - "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -3713,11 +3669,6 @@ resolved "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.24.0.tgz#b6e83333c437ac106386e10a776dc823c1a90f33" integrity sha512-kfYxX0rHP5N2Da6HyfjRCVaeNahAO9XV5WD4SKWKKjdKVkV/Z5/XjVgSKlTBLSYxnWDzYJJ4UHZV43Mw+facMA== -"@petamoriken/float16@^3.4.7": - version "3.7.1" - resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.7.1.tgz#4a0cc0854a3a101cc2d697272f120e1a05975ce5" - integrity sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw== - "@philpl/buble@^0.19.7": version "0.19.7" resolved "https://registry.npmjs.org/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893" @@ -7663,13 +7614,6 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.19: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== -colormap@^2.3: - version "2.3.2" - resolved "https://registry.yarnpkg.com/colormap/-/colormap-2.3.2.tgz#4422c1178ce563806e265b96782737be85815abf" - integrity sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA== - dependencies: - lerp "^1.0.3" - colors@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -7753,11 +7697,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -complex.js@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" - integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg== - component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -8393,11 +8332,6 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -csscolorparser@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" - integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== - cssdb@^7.1.0: version "7.5.4" resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.5.4.tgz#e34dafee5184d67634604e345e389ca79ac179ea" @@ -8746,17 +8680,6 @@ dayjs@^1.10.4: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -dcmjs@^0.27: - version "0.27.0" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.27.0.tgz#2662818c8b20494e366583e6dd3577c20d04d6ff" - integrity sha512-26wtatOLh+0b0aFy9iOg7PdOLG9EHevn9nEOn7Aoo5l7P9aFAMZ3XAa9Q+NULzLE2Q7DcIf2TQvfyVtdzhQzeg== - dependencies: - "@babel/runtime-corejs2" "^7.17.8" - gl-matrix "^3.1.0" - lodash.clonedeep "^4.5.0" - loglevelnext "^3.0.1" - ndarray "^1.0.19" - dcmjs@^0.29.5: version "0.29.6" resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.6.tgz#6ca1543e74bae29657d1f7f2273407e23266c011" @@ -8820,11 +8743,6 @@ decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.4.3: - version "10.4.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -9150,37 +9068,15 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" -dicom-microscopy-viewer@^0.44.0: - version "0.44.0" - resolved "https://registry.npmjs.org/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.44.0.tgz#d4a9e985acb23c5b82a9aedbe379b39198b8fd55" - integrity sha512-7rcm8bXTcOLsXrhBPwWe5gf4Sj1Rz1lRh+ECcaznOEr/uvX6BTAYLqNJNmeAUMoKcadboEeDrmLihCwCteRSJQ== - dependencies: - "@cornerstonejs/codec-charls" "^0.1.1" - "@cornerstonejs/codec-libjpeg-turbo-8bit" "^0.0.7" - "@cornerstonejs/codec-openjpeg" "^0.1.1" - colormap "^2.3" - dcmjs "^0.27" - dicomicc "^0.1" - dicomweb-client "^0.8" - image-type "^4.1" - mathjs "^11.2" - ol "^7.1" - uuid "^9.0" - dicom-parser@^1.8.9: version "1.8.21" resolved "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.21.tgz#916fdc77776367976b8457cad462b5b7cf74eaea" integrity sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w== -dicomicc@^0.1: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dicomicc/-/dicomicc-0.1.0.tgz#c73acc60a8e2d73a20f462c8c7d0e1e0d977c486" - integrity sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ== - -dicomweb-client@^0.8, dicomweb-client@^0.8.4: - version "0.8.4" - resolved "https://registry.npmjs.org/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74" - integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw== +dicomweb-client@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.2.tgz#9c2e466264a5d3b56c18edaafd360e89912cac13" + integrity sha512-sGjq3TxM7jgbe7cBpqddT1VlfOlwXmE7Q12qCSGDOkEL7fgn+Ak/5oW/LiRP2FzmfYzcbpO32r+hC8i4ITXQLw== didyoumean@^1.2.2: version "1.2.2" @@ -9495,11 +9391,6 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -earcut@^2.2.3: - version "2.2.4" - resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" - integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -9812,11 +9703,6 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-latex@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" - integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== - escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -10780,11 +10666,6 @@ file-system-cache@^2.0.0: fs-extra "^11.1.0" ramda "^0.28.0" -file-type@^10.10.0: - version "10.11.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890" - integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw== - file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -11252,19 +11133,6 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -geotiff@^2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz#358e578233af70bfb0b4dee62d599ad78fc5cfca" - integrity sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q== - dependencies: - "@petamoriken/float16" "^3.4.7" - lerc "^3.0.0" - pako "^2.0.4" - parse-headers "^2.0.2" - quick-lru "^6.1.1" - web-worker "^1.2.0" - xml-utils "^1.0.2" - get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -12283,7 +12151,7 @@ identity-obj-proxy@3.0.x: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.12, ieee754@^1.1.13: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -12317,13 +12185,6 @@ image-size@^1.0.1: dependencies: queue "6.0.2" -image-type@^4.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/image-type/-/image-type-4.1.0.tgz#72a88d64ff5021371ed67b9a466442100be57cd1" - integrity sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg== - dependencies: - file-type "^10.10.0" - immer@^9.0.7: version "9.0.21" resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -13266,11 +13127,6 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -javascript-natural-sort@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" - integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== - jest-canvas-mock@^2.1.0: version "2.5.0" resolved "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.0.tgz#3e60f87f77ddfa273cf8e7e4ea5f86fa827c7117" @@ -13896,11 +13752,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-pretty-compact@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885" - integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -14077,11 +13928,6 @@ left-pad@^1.3.0: resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== -lerc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb" - integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww== - lerna@^3.15.0: version "3.22.1" resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" @@ -14106,11 +13952,6 @@ lerna@^3.15.0: import-local "^2.0.0" npmlog "^4.1.2" -lerp@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/lerp/-/lerp-1.0.3.tgz#a18c8968f917896de15ccfcc28d55a6b731e776e" - integrity sha512-70Rh4rCkJDvwWiTsyZ1HmJGvnyfFah4m6iTux29XmasRiZPDBpT9Cfa4ai73+uLZxnlKruUS62jj2lb11wURiA== - leven@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -14635,11 +14476,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -mapbox-to-css-font@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz#a9e31b363ad8ca881cd339ca99f2d2a6b02ea5dd" - integrity sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA== - markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" @@ -14660,21 +14496,6 @@ material-colors@^1.2.1: resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== -mathjs@^11.2: - version "11.8.0" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-11.8.0.tgz#b02e66461ec068fadf1e90c221121704dc14d8f5" - integrity sha512-I7r8HCoqUGyEiHQdeOCF2m2k9N+tcOHO3cZQ3tyJkMMBQMFqMR7dMQEboBMJAiFW2Um3PEItGPwcOc4P6KRqwg== - dependencies: - "@babel/runtime" "^7.21.0" - complex.js "^2.1.1" - decimal.js "^10.4.3" - escape-latex "^1.2.0" - fraction.js "^4.2.0" - javascript-natural-sort "^0.7.1" - seedrandom "^3.0.5" - tiny-emitter "^2.1.0" - typed-function "^4.1.0" - mdast-squeeze-paragraphs@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" @@ -16135,25 +15956,6 @@ oidc-client@1.11.5: crypto-js "^4.0.0" serialize-javascript "^4.0.0" -ol-mapbox-style@^9.2.0: - version "9.7.0" - resolved "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz#38a4f7abc8f0a94f378dcdb7cefdcc69ca3f6287" - integrity sha512-YX3u8FBJHsRHaoGxmd724Mp5WPTuV7wLQW6zZhcihMuInsSdCX1EiZfU+8IAL7jG0pbgl5YgC0aWE/MXJcUXxg== - dependencies: - "@mapbox/mapbox-gl-style-spec" "^13.23.1" - mapbox-to-css-font "^2.4.1" - -ol@^7.1: - version "7.3.0" - resolved "https://registry.npmjs.org/ol/-/ol-7.3.0.tgz#7ffb5f258dafa4a3e218208aad9054d61f6fe786" - integrity sha512-08vJE4xITKPazQ9qJjeqYjRngnM9s+1eSv219Pdlrjj3LpLqjEH386ncq+76Dw1oGPGR8eLVEePk7FEd9XqqMw== - dependencies: - earcut "^2.2.3" - geotiff "^2.0.7" - ol-mapbox-style "^9.2.0" - pbf "3.2.1" - rbush "^3.0.1" - on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -16499,11 +16301,6 @@ parse-github-repo-url@^1.3.0: resolved "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" integrity sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg== -parse-headers@^2.0.2: - version "2.0.5" - resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9" - integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -16704,14 +16501,6 @@ pause-stream@0.0.11: dependencies: through "~2.3" -pbf@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" - integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== - dependencies: - ieee754 "^1.1.12" - resolve-protobuf-schema "^2.1.0" - peek-stream@^1.1.0: version "1.1.3" resolved "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" @@ -17939,11 +17728,6 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -protocol-buffers-schema@^3.3.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" - integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== - protocols@^1.4.0: version "1.4.8" resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" @@ -18135,16 +17919,6 @@ quick-lru@^5.1.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -quick-lru@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz#f8e5bf9010376c126c80c1a62827a526c0e60adf" - integrity sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q== - -quickselect@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" - integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== - raf@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -18184,13 +17958,6 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -rbush@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" - integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== - dependencies: - quickselect "^2.0.0" - rc@1.2.8, rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -19276,13 +19043,6 @@ resolve-pathname@^3.0.0: resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve-protobuf-schema@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" - integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== - dependencies: - protocol-buffers-schema "^3.3.1" - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -19459,11 +19219,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rw@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - rxjs@^6.3.3, rxjs@^6.4.0: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -19613,7 +19368,7 @@ section-matter@^1.0.0: extend-shallow "^2.0.1" kind-of "^6.0.0" -seedrandom@3.0.5, seedrandom@^3.0.5: +seedrandom@3.0.5: version "3.0.5" resolved "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== @@ -20058,21 +19813,11 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" -sort-asc@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9" - integrity sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw== - sort-css-media-queries@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== -sort-desc@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.1.1.tgz#198b8c0cdeb095c463341861e3925d4ee359a9ee" - integrity sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw== - sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -20087,14 +19832,6 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -sort-object@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-0.3.2.tgz#98e0d199ede40e07c61a84403c61d6c3b290f9e2" - integrity sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA== - dependencies: - sort-asc "^0.1.0" - sort-desc "^0.1.1" - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -21086,11 +20823,6 @@ timsort@^0.3.0: resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== -tiny-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -21352,11 +21084,6 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typed-function@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz#da4bdd8a6d19a89e22732f75e4a410860aaf9712" - integrity sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -21853,7 +21580,7 @@ uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0, uuid@^9.0.0: +uuid@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== @@ -22052,11 +21779,6 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== -web-worker@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" - integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== - webgl-constants@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855" @@ -22777,11 +22499,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml-utils@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.3.0.tgz#f1043534e3ac3deda12ddab39f8442e16da98ebb" - integrity sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA== - xml@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"